mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 21:33:02 +01:00
* feat: begin switching sonner, currently this breaks all alerts * feat: switch to using new sonner and fix class names * feat: add Shortcut component for improved keyboard shortcuts display in default layout * feat: rewrite quick menu modal in shadcn * feat: update QuickMenu modal placeholders and localize no results message in default layout * feat: begin switching modals in layout to use shadcn dialog, needs bug fixing * feat: implement DialogProvider for consistent dialog management across components * fix: types * feat: begin adding shadcn label selector (wip) * feat: shadcnify textarea * feat: begin adding location selector * feat: add hotkey support for opening create modals in dialog provider components * fix: update click event on NuxtLink and reorder sidebar menu item IDs for consistency * feat: unify shortcut text across create modals and sort issue with text centring * feat: prevent dialog from opening when a dialog alert is open * fix: prevent potential out of bounds error * feat: enhance button group UI in create modals for better layout and introduce new item photo label in the form * fix: search on label selector * chore: lint * fix: oops * feat: make selector usable * feat: add actual data to label selector * feat: label selector kinda works * fix: add legacy selector for edit page * fix: enable camera capture in image upload for CreateModal component * fix: z levels for sidebar mobile * fix: gap between inputs * feat: update radix-vue, custom search function for location selector * feat: add fuzzysort (can always remove it and go to lunr if we want to) * feat: limit label name to 50 characters in create modal and selector, helps with issues with ui not working with larger labels, as it is only enforced on the frontend could be easily bypassed but thats a them problem * feat: add colours to toast * chore: lint * feat: abstract the dialog for creation modals * feat: add drawer component and responsive dialog for create modals * feat: enhance photo preview in CreateModal * fix: remember state of sidebar * feat: add ui functionality for changing primary image * feat: use button for file upload * style: lint * fix: dont clone asset id * fix: using create and add label breaks selector * chore: oops remove logging * chore: lint * fix: cut length of label dramatically to ensure maximal compatibility, not sure if too much * fix: more limiting of label length * feat: update reka-ui (prev radix-vue) * chore: cleanup dialog provider and siebar provider a bit * fix: improve accessibility * fix: docs for shadcn error * fix: hack to prevent issues with lots of toasts in quick succession * feat: cleanup toast file and lint * feat: improvements to dialog scroll and disable the ability to set default photo for now * feat: add tooltips for photo buttons * chore: substring to length check Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
302 lines
9.4 KiB
Vue
302 lines
9.4 KiB
Vue
<script setup lang="ts">
|
|
import { toast } from "@/components/ui/sonner";
|
|
import MdiGithub from "~icons/mdi/github";
|
|
import MdiDiscord from "~icons/mdi/discord";
|
|
import MdiFolder from "~icons/mdi/folder";
|
|
import MdiAccount from "~icons/mdi/account";
|
|
import MdiAccountPlus from "~icons/mdi/account-plus";
|
|
import MdiLogin from "~icons/mdi/login";
|
|
import MdiArrowRight from "~icons/mdi/arrow-right";
|
|
import MdiLock from "~icons/mdi/lock";
|
|
import MdiMastodon from "~icons/mdi/mastodon";
|
|
|
|
useHead({
|
|
title: "Homebox | Organize and Tag Your Stuff",
|
|
});
|
|
|
|
definePageMeta({
|
|
layout: "empty",
|
|
middleware: [
|
|
() => {
|
|
const ctx = useAuthContext();
|
|
if (ctx.isAuthorized()) {
|
|
return "/home";
|
|
}
|
|
},
|
|
],
|
|
});
|
|
|
|
const ctx = useAuthContext();
|
|
|
|
const api = usePublicApi();
|
|
|
|
const { data: status } = useAsyncData(async () => {
|
|
const { data } = await api.status();
|
|
|
|
if (data.demo) {
|
|
username.value = "demo@example.com";
|
|
password.value = "demo";
|
|
}
|
|
return data;
|
|
});
|
|
|
|
whenever(status, status => {
|
|
if (status?.demo) {
|
|
email.value = "demo@example.com";
|
|
loginPassword.value = "demo";
|
|
}
|
|
});
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
const username = ref("");
|
|
const email = ref("");
|
|
const password = ref("");
|
|
const canRegister = ref(false);
|
|
const remember = ref(false);
|
|
|
|
const groupToken = computed<string>({
|
|
get() {
|
|
const params = route.query.token;
|
|
|
|
if (typeof params === "string") {
|
|
return params;
|
|
}
|
|
|
|
return "";
|
|
},
|
|
set(v) {
|
|
router.push({
|
|
query: {
|
|
token: v,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
async function registerUser() {
|
|
loading.value = true;
|
|
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
if (!emailRegex.test(email.value)) {
|
|
toast.error("Invalid email address");
|
|
loading.value = false;
|
|
return;
|
|
}
|
|
|
|
const { error } = await api.register({
|
|
name: username.value,
|
|
email: email.value,
|
|
password: password.value,
|
|
token: groupToken.value,
|
|
});
|
|
|
|
if (error) {
|
|
toast.error("Problem registering user");
|
|
return;
|
|
}
|
|
|
|
toast.success("User registered");
|
|
|
|
loading.value = false;
|
|
registerForm.value = false;
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (groupToken.value !== "") {
|
|
registerForm.value = true;
|
|
}
|
|
});
|
|
|
|
const loading = ref(false);
|
|
const loginPassword = ref("");
|
|
const redirectTo = useState("authRedirect");
|
|
|
|
async function login() {
|
|
loading.value = true;
|
|
const { error } = await ctx.login(api, email.value, loginPassword.value, remember.value);
|
|
|
|
if (error) {
|
|
toast.error("Invalid email or password");
|
|
loading.value = false;
|
|
return;
|
|
}
|
|
|
|
toast.success("Logged in successfully");
|
|
|
|
navigateTo(redirectTo.value || "/home");
|
|
redirectTo.value = null;
|
|
loading.value = false;
|
|
}
|
|
|
|
const [registerForm, toggleLogin] = useToggle();
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex min-h-screen flex-col">
|
|
<div class="absolute top-0 min-w-full fill-primary">
|
|
<div class="flex min-h-[20vh] flex-col bg-primary" />
|
|
<svg
|
|
class="fill-primary drop-shadow-xl"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 1440 320"
|
|
preserveAspectRatio="none"
|
|
>
|
|
<path
|
|
fill-opacity="1"
|
|
d="M0,32L80,69.3C160,107,320,181,480,181.3C640,181,800,107,960,117.3C1120,128,1280,224,1360,272L1440,320L1440,0L1360,0C1280,0,1120,0,960,0C800,0,640,0,480,0C320,0,160,0,80,0L0,0Z"
|
|
></path>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<header class="mx-auto p-4 sm:flex sm:items-end sm:p-6 lg:p-14">
|
|
<div class="z-10">
|
|
<h2 class="mt-1 flex text-4xl font-bold tracking-tight text-neutral-content sm:text-5xl lg:text-6xl">
|
|
HomeB
|
|
<AppLogo class="-mb-4 w-12" />
|
|
x
|
|
</h2>
|
|
<p class="ml-1 text-lg text-base-content/50">{{ $t("index.tagline") }}</p>
|
|
</div>
|
|
<div class="ml-auto mt-6 flex gap-4 text-neutral-content sm:mt-0">
|
|
<a
|
|
class="tooltip"
|
|
:data-tip="$t('global.github')"
|
|
href="https://github.com/sysadminsmedia/homebox"
|
|
target="_blank"
|
|
>
|
|
<MdiGithub class="size-8" />
|
|
</a>
|
|
<a
|
|
href="https://noc.social/@sysadminszone"
|
|
class="tooltip"
|
|
:data-tip="$t('global.follow_dev')"
|
|
target="_blank"
|
|
>
|
|
<MdiMastodon class="size-8" />
|
|
</a>
|
|
<a href="https://discord.gg/aY4DCkpNA9" class="tooltip" :data-tip="$t('global.join_discord')" target="_blank">
|
|
<MdiDiscord class="size-8" />
|
|
</a>
|
|
<a href="https://homebox.software/en/" class="tooltip" :data-tip="$t('global.read_docs')" target="_blank">
|
|
<MdiFolder class="size-8" />
|
|
</a>
|
|
</div>
|
|
</header>
|
|
<div class="grid min-h-[50vh] p-6 sm:place-items-center">
|
|
<div>
|
|
<Transition name="slide-fade">
|
|
<form v-if="registerForm" @submit.prevent="registerUser">
|
|
<div class="card bg-base-100 shadow-xl md:w-[500px]">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl">
|
|
<MdiAccount class="mr-1 size-7" />
|
|
{{ $t("index.register") }}
|
|
</h2>
|
|
<FormTextField v-model="email" :label="$t('index.set_email')" />
|
|
<FormTextField v-model="username" :label="$t('index.set_name')" />
|
|
<div v-if="!(groupToken == '')" class="pb-1 pt-4 text-center">
|
|
<p>{{ $t("index.joining_group") }}</p>
|
|
<button type="button" class="text-xs underline" @click="groupToken = ''">
|
|
{{ $t("index.dont_join_group") }}
|
|
</button>
|
|
</div>
|
|
<FormPassword v-model="password" :label="$t('index.set_password')" />
|
|
<PasswordScore v-model:valid="canRegister" :password="password" />
|
|
<div class="card-actions justify-end">
|
|
<button
|
|
type="submit"
|
|
class="btn btn-primary mt-2"
|
|
:class="loading ? 'loading' : ''"
|
|
:disabled="loading || !canRegister"
|
|
>
|
|
{{ $t("index.register") }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
<form v-else @submit.prevent="login">
|
|
<div class="card bg-base-100 shadow-xl md:w-[500px]">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl">
|
|
<MdiAccount class="mr-1 size-7" />
|
|
{{ $t("index.login") }}
|
|
</h2>
|
|
<template v-if="status && status.demo">
|
|
<p class="text-center text-xs italic">This is a demo instance</p>
|
|
<p class="text-center text-xs">
|
|
<b>{{ $t("global.email") }}</b> demo@example.com
|
|
</p>
|
|
<p class="text-center text-xs">
|
|
<b>{{ $t("global.password") }}</b> demo
|
|
</p>
|
|
</template>
|
|
<FormTextField v-model="email" :label="$t('global.email')" />
|
|
<FormPassword v-model="loginPassword" :label="$t('global.password')" />
|
|
<div class="max-w-[140px]">
|
|
<FormCheckbox v-model="remember" :label="$t('index.remember_me')" />
|
|
</div>
|
|
<div class="card-actions justify-end">
|
|
<button
|
|
type="submit"
|
|
class="btn btn-primary btn-block"
|
|
:class="loading ? 'loading' : ''"
|
|
:disabled="loading"
|
|
>
|
|
{{ $t("index.login") }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</Transition>
|
|
<div class="mt-6 text-center">
|
|
<BaseButton
|
|
v-if="status && status.allowRegistration"
|
|
class="btn-primary btn-wide"
|
|
@click="() => toggleLogin()"
|
|
>
|
|
<template #icon>
|
|
<MdiAccountPlus v-if="!registerForm" class="swap-off size-5" />
|
|
<MdiLogin v-else class="swap-off size-5" />
|
|
<MdiArrowRight class="swap-on size-5" />
|
|
</template>
|
|
{{ registerForm ? $t("index.login") : $t("index.register") }}
|
|
</BaseButton>
|
|
<p v-else class="inline-flex items-center gap-2 text-sm italic text-base-content">
|
|
<MdiLock class="inline-block size-4" />
|
|
{{ $t("index.disabled_registration") }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<footer v-if="status" class="bottom-0 mt-auto w-full pb-4 text-center">
|
|
<p class="text-center text-sm">
|
|
{{ $t("global.version", { version: status.build.version }) }} ~
|
|
{{ $t("global.build", { build: status.build.commit }) }}
|
|
</p>
|
|
</footer>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="css" scoped>
|
|
.slide-fade-enter-active {
|
|
transition: all 0.2s ease-out;
|
|
}
|
|
|
|
.slide-fade-enter-from,
|
|
.slide-fade-leave-to {
|
|
position: absolute;
|
|
transform: translateX(20px);
|
|
opacity: 0;
|
|
}
|
|
|
|
progress[value]::-webkit-progress-value {
|
|
transition: width 0.5s;
|
|
}
|
|
</style>
|