Browse Source

better login screen

pull/1666/head
Bernd Storath 6 months ago
parent
commit
4c463af078
  1. 2
      src/app/components/Clients/New.vue
  2. 10
      src/app/components/base/Input.vue
  3. 2
      src/app/components/form/ActionField.vue
  4. 8
      src/app/components/form/DateField.vue
  5. 8
      src/app/components/form/NullTextField.vue
  6. 8
      src/app/components/form/NumberField.vue
  7. 3
      src/app/components/form/PasswordField.vue
  8. 8
      src/app/components/form/TextField.vue
  9. 8
      src/app/pages/clients/[id].vue
  10. 71
      src/app/pages/login.vue
  11. 12
      src/i18n/locales/en.json

2
src/app/components/Clients/New.vue

@ -1,6 +1,6 @@
<template> <template>
<ClientsCreateDialog> <ClientsCreateDialog>
<BaseButton> <BaseButton as="span">
<IconsPlus class="w-4 md:mr-2" /> <IconsPlus class="w-4 md:mr-2" />
<span class="text-sm max-md:hidden">{{ $t('new') }}</span> <span class="text-sm max-md:hidden">{{ $t('new') }}</span>
</BaseButton> </BaseButton>

10
src/app/components/base/Input.vue

@ -0,0 +1,10 @@
<template>
<input
v-model="data"
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
/>
</template>
<script lang="ts" setup>
const data = defineModel<unknown>();
</script>

2
src/app/components/form/ActionField.vue

@ -2,7 +2,7 @@
<input <input
:value="label" :value="label"
:type="type ?? 'button'" :type="type ?? 'button'"
class="col-span-2 rounded-lg border-2 border-gray-100 py-2 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" class="col-span-2 rounded-lg border-2 border-gray-100 py-2 text-gray-500 hover:border-red-800 hover:bg-red-800 hover:text-white focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
/> />
</template> </template>

8
src/app/components/form/DateField.vue

@ -2,13 +2,7 @@
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> <Label :for="id" class="font-semibold md:align-middle md:leading-10">
{{ label }} {{ label }}
</Label> </Label>
<input <BaseInput :id="id" v-model="data" :name="id" type="date" />
:id="id"
v-model="data"
:name="id"
type="date"
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

8
src/app/components/form/NullTextField.vue

@ -7,13 +7,7 @@
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
</div> </div>
<input <BaseInput :id="id" v-model.trim="data" :name="id" type="text" />
:id="id"
v-model.trim="data"
:name="id"
type="text"
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

8
src/app/components/form/NumberField.vue

@ -7,13 +7,7 @@
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
</div> </div>
<input <BaseInput :id="id" v-model.number="data" :name="id" type="number" />
:id="id"
v-model.number="data"
:name="id"
type="number"
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

3
src/app/components/form/PasswordField.vue

@ -2,13 +2,12 @@
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> <Label :for="id" class="font-semibold md:align-middle md:leading-10">
{{ label }} {{ label }}
</Label> </Label>
<input <BaseInput
:id="id" :id="id"
v-model.trim="data" v-model.trim="data"
:name="id" :name="id"
type="password" type="password"
:autocomplete="autocomplete" :autocomplete="autocomplete"
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
/> />
</template> </template>

8
src/app/components/form/TextField.vue

@ -7,13 +7,7 @@
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
</div> </div>
<input <BaseInput :id="id" v-model.trim="data" :name="id" type="text" />
:id="id"
v-model.trim="data"
:name="id"
type="text"
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

8
src/app/pages/clients/[id].vue

@ -73,7 +73,13 @@
:client-name="data.name" :client-name="data.name"
@delete="deleteClient" @delete="deleteClient"
> >
<FormActionField label="Delete" class="w-full" /> <FormActionField
label="Delete"
class="w-full"
type="button"
tabindex="-1"
as="span"
/>
</ClientsDeleteDialog> </ClientsDeleteDialog>
</FormGroup> </FormGroup>
</FormElement> </FormElement>

71
src/app/pages/login.vue

@ -2,83 +2,58 @@
<main> <main>
<UiBanner /> <UiBanner />
<form <form
class="mx-auto mt-10 w-64 overflow-hidden rounded-md bg-white p-5 text-gray-700 shadow dark:bg-neutral-700 dark:text-neutral-200" class="mx-auto mt-10 flex w-64 flex-col gap-5 overflow-hidden rounded-md bg-white p-5 text-gray-700 shadow dark:bg-neutral-700 dark:text-neutral-200"
@submit="login" @submit.prevent="login"
> >
<!-- Avatar --> <!-- Avatar -->
<div <div
class="relative mx-auto mb-10 mt-5 h-20 w-20 overflow-hidden rounded-full bg-red-800 dark:bg-red-800" class="mx-auto mb-5 mt-5 h-20 w-20 overflow-hidden rounded-full bg-red-800 dark:bg-red-800"
> >
<IconsAvatar class="m-5 h-10 w-10 text-white dark:text-white" /> <IconsAvatar class="m-5 h-10 w-10 text-white dark:text-white" />
</div> </div>
<input <BaseInput
v-model="username" v-model="username"
type="text" type="text"
name="username" :placeholder="$t('general.username')"
:placeholder="$t('username')"
autocomplete="username" autocomplete="username"
autofocus autofocus
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-500 dark:placeholder:text-neutral-400 dark:focus:border-red-800" name="username"
/> />
<input <BaseInput
v-model="password" v-model="password"
type="password" type="password"
name="password" name="password"
:placeholder="$t('password')" :placeholder="$t('general.password')"
autocomplete="current-password" autocomplete="current-password"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-500 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/> />
<label <label
class="mb-5 inline-block cursor-pointer whitespace-nowrap" class="flex gap-2 whitespace-nowrap"
:title="$t('titleRememberMe')" :title="$t('login.rememberMeDesc')"
> >
<input v-model="remember" type="checkbox" class="sr-only" /> <BaseSwitch v-model="remember" />
<span class="text-sm">{{ $t('login.rememberMe') }}</span>
<div
v-if="remember"
class="mr-1 inline-block h-6 w-10 cursor-pointer rounded-full bg-red-800 align-middle transition-all hover:bg-red-700"
>
<div class="m-1 ml-5 h-4 w-4 rounded-full bg-white"></div>
</div>
<div
v-if="!remember"
class="mr-1 inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-200 align-middle transition-all hover:bg-gray-300 dark:bg-neutral-400 dark:hover:bg-neutral-500"
>
<div class="m-1 h-4 w-4 rounded-full bg-white"></div>
</div>
<span class="text-sm">{{ $t('rememberMe') }}</span>
</label> </label>
<button <button
v-if="authenticating" class="rounded py-2 text-sm text-white shadow transition dark:text-white"
class="w-full cursor-not-allowed rounded bg-red-800 py-2 text-sm text-white shadow dark:bg-red-800 dark:text-white" :class="{
'cursor-pointer bg-red-800 hover:bg-red-700 dark:bg-red-800 dark:hover:bg-red-700':
password && username,
'cursor-not-allowed bg-gray-200 dark:bg-neutral-800':
!password || !username,
}"
> >
<IconsLoading class="mx-auto w-5 animate-spin" /> <IconsLoading v-if="authenticating" class="mx-auto w-5 animate-spin" />
<span v-else>{{ $t('login.signIn') }}</span>
</button> </button>
<input
v-else
type="submit"
:class="[
{
'cursor-pointer bg-red-800 transition hover:bg-red-700 dark:bg-red-800 dark:hover:bg-red-700':
password,
'cursor-not-allowed bg-gray-200 dark:bg-neutral-800': !password,
},
'w-full rounded py-2 text-sm text-white shadow dark:text-white',
]"
:value="$t('signIn')"
/>
</form> </form>
</main> </main>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// TODO: improve
import { FetchError } from 'ofetch'; import { FetchError } from 'ofetch';
const { t } = useI18n(); const { t } = useI18n();
@ -90,9 +65,7 @@ const password = ref<null | string>(null);
const authStore = useAuthStore(); const authStore = useAuthStore();
const toast = useToast(); const toast = useToast();
async function login(e: Event) { async function login() {
e.preventDefault();
if (!username.value || !password.value || authenticating.value) return; if (!username.value || !password.value || authenticating.value) return;
authenticating.value = true; authenticating.value = true;

12
src/i18n/locales/en.json

@ -19,6 +19,7 @@
}, },
"general": { "general": {
"name": "Name", "name": "Name",
"username": "Username",
"password": "Password", "password": "Password",
"newPassword": "New Password", "newPassword": "New Password",
"updatePassword": "Update Password", "updatePassword": "Update Password",
@ -51,9 +52,6 @@
"portPlaceholder": "443", "portPlaceholder": "443",
"migration": "Restore the backup" "migration": "Restore the backup"
}, },
"name": "Name",
"username": "Username",
"signIn": "Sign In",
"logout": "Logout", "logout": "Logout",
"updateAvailable": "There is an update available!", "updateAvailable": "There is an update available!",
"update": "Update", "update": "Update",
@ -77,12 +75,15 @@
"light": "Light theme", "light": "Light theme",
"system": "System theme" "system": "System theme"
}, },
"rememberMe": "Remember me",
"titleRememberMe": "Stay logged after closing the browser",
"sort": "Sort", "sort": "Sort",
"Permanent": "Permanent", "Permanent": "Permanent",
"OneTimeLink": "Generate short one time link", "OneTimeLink": "Generate short one time link",
"errorInit": "Initialization failed.", "errorInit": "Initialization failed.",
"login": {
"signIn": "Sign In",
"rememberMe": "Remember me",
"rememberMeDesc": "Stay logged after closing the browser"
},
"error": { "error": {
"clear": "Clear", "clear": "Clear",
"login": "Log in error" "login": "Log in error"
@ -117,7 +118,6 @@
"sectionGeneral": "General", "sectionGeneral": "General",
"sectionAdvanced": "Advanced" "sectionAdvanced": "Advanced"
}, },
"password": "Password",
"admin": { "admin": {
"general": { "general": {
"sessionTimeout": "Session Timeout", "sessionTimeout": "Session Timeout",

Loading…
Cancel
Save