diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index 7aa1c76ed3..8bd955734a 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -5,10 +5,8 @@ import { ByteUnit, convertToBytes } from '$lib/utils/byte-units'; import { handleError } from '$lib/utils/handle-error'; import { createUserAdmin } from '@immich/sdk'; + import { Alert, Button, Field, HelperText, Input, PasswordInput, Stack, Switch } from '@immich/ui'; import { t } from 'svelte-i18n'; - import Button from '../elements/buttons/button.svelte'; - import Slider from '../elements/slider.svelte'; - import PasswordField from '../shared-components/password-field.svelte'; interface Props { onClose: () => void; @@ -17,137 +15,114 @@ oauthEnabled?: boolean; } - let { onClose, onSubmit, onCancel, oauthEnabled = false }: Props = $props(); + let { onClose, onSubmit: onDone, onCancel, oauthEnabled = false }: Props = $props(); let error = $state(''); - let success = $state(''); + let success = $state(false); let email = $state(''); let password = $state(''); - let confirmPassword = $state(''); + let passwordConfirm = $state(''); let name = $state(''); let shouldChangePassword = $state(true); let notify = $state(true); - let canCreateUser = $state(false); - let quotaSize: number | undefined = $state(); + let quotaSize: string | undefined = $state(); let isCreatingUser = $state(false); - let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(quotaSize, ByteUnit.GiB) : null); + let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(Number(quotaSize), ByteUnit.GiB) : null); let quotaSizeWarning = $derived( quotaSizeInBytes && userInteraction.serverInfo && quotaSizeInBytes > userInteraction.serverInfo.diskSizeRaw, ); - $effect(() => { - if (password !== confirmPassword && confirmPassword.length > 0) { - error = $t('password_does_not_match'); - canCreateUser = false; - } else { - error = ''; - canCreateUser = true; - } - }); + const passwordMismatch = $derived(password !== passwordConfirm && passwordConfirm.length > 0); + const passwordMismatchMessage = $derived(passwordMismatch ? $t('password_does_not_match') : ''); + const valid = $derived(!passwordMismatch && !isCreatingUser); - async function registerUser() { - if (canCreateUser && !isCreatingUser) { - isCreatingUser = true; - error = ''; - - try { - await createUserAdmin({ - userAdminCreateDto: { - email, - password, - shouldChangePassword, - name, - quotaSizeInBytes, - notify, - }, - }); - - success = $t('new_user_created'); - - onSubmit(); - - return; - } catch (error) { - handleError(error, $t('errors.unable_to_create_user')); - } finally { - isCreatingUser = false; - } - } - } - - const onsubmit = async (event: Event) => { + const onSubmit = async (event: Event) => { event.preventDefault(); - await registerUser(); + + if (!valid) { + return; + } + + isCreatingUser = true; + error = ''; + + try { + await createUserAdmin({ + userAdminCreateDto: { + email, + password, + shouldChangePassword, + name, + quotaSizeInBytes, + notify, + }, + }); + + success = true; + + onDone(); + + return; + } catch (error) { + handleError(error, $t('errors.unable_to_create_user')); + } finally { + isCreatingUser = false; + } }; </script> -<FullScreenModal title={$t('create_new_user')} showLogo {onClose}> - <form {onsubmit} autocomplete="off" id="create-new-user-form"> - <div class="my-4 flex flex-col gap-2"> - <label class="immich-form-label" for="email">{$t('email')}</label> - <input class="immich-form-input" id="email" bind:value={email} type="email" required /> - </div> - - {#if $featureFlags.email} - <div class="my-4 flex place-items-center justify-between gap-2"> - <label class="text-sm dark:text-immich-dark-fg" for="send-welcome-email"> - {$t('admin.send_welcome_email')} - </label> - <Slider id="send-welcome-email" bind:checked={notify} /> - </div> - {/if} - - <div class="my-4 flex flex-col gap-2"> - <label class="immich-form-label" for="password">{$t('password')}</label> - <PasswordField id="password" bind:password autocomplete="new-password" required={!oauthEnabled} /> - </div> - - <div class="my-4 flex flex-col gap-2"> - <label class="immich-form-label" for="confirmPassword">{$t('confirm_password')}</label> - <PasswordField - id="confirmPassword" - bind:password={confirmPassword} - autocomplete="new-password" - required={!oauthEnabled} - /> - </div> - - <div class="my-4 flex place-items-center justify-between gap-2"> - <label class="text-sm dark:text-immich-dark-fg" for="require-password-change"> - {$t('admin.require_password_change_on_login')} - </label> - <Slider id="require-password-change" bind:checked={shouldChangePassword} /> - </div> - - <div class="my-4 flex flex-col gap-2"> - <label class="immich-form-label" for="name">{$t('name')}</label> - <input class="immich-form-input" id="name" bind:value={name} type="text" required /> - </div> - - <div class="my-4 flex flex-col gap-2"> - <label class="flex items-center gap-2 immich-form-label" for="quotaSize"> - {$t('admin.quota_size_gib')} - {#if quotaSizeWarning} - <p class="text-red-400 text-sm">{$t('errors.quota_higher_than_disk_size')}</p> - {/if} - </label> - <input class="immich-form-input" id="quotaSize" type="number" min="0" bind:value={quotaSize} /> - </div> - +<form onsubmit={onSubmit} autocomplete="off" id="create-new-user-form"> + <FullScreenModal title={$t('create_new_user')} showLogo {onClose}> {#if error} - <p class="text-sm text-red-400">{error}</p> + <Alert color="danger" size="small" title={error} closable /> {/if} {#if success} - <p class="text-sm text-immich-primary">{success}</p> + <p class="text-sm text-immich-primary">{$t('new_user_created')}</p> {/if} - </form> - {#snippet stickyBottom()} - <Button color="gray" fullwidth onclick={onCancel}>{$t('cancel')}</Button> - <Button type="submit" disabled={isCreatingUser} fullwidth form="create-new-user-form">{$t('create')}</Button> - {/snippet} -</FullScreenModal> + <Stack gap={4}> + <Field label={$t('email')} required> + <Input bind:value={email} type="email" /> + </Field> + + {#if $featureFlags.email} + <Field label={$t('admin.send_welcome_email')}> + <Switch id="send-welcome-email" bind:checked={notify} class="flex justify-between text-sm" /> + </Field> + {/if} + + <Field label={$t('password')} required={!oauthEnabled}> + <PasswordInput id="password" bind:value={password} autocomplete="new-password" /> + </Field> + + <Field label={$t('confirm_password')} required={!oauthEnabled}> + <PasswordInput id="confirmPassword" bind:value={passwordConfirm} autocomplete="new-password" /> + <HelperText color="danger">{passwordMismatchMessage}</HelperText> + </Field> + + <Field label={$t('admin.require_password_change_on_login')}> + <Switch id="require-password-change" bind:checked={shouldChangePassword} class="flex justify-between text-sm" /> + </Field> + + <Field label={$t('name')} required> + <Input bind:value={name} /> + </Field> + + <Field label={$t('admin.quota_size_gib')}> + <Input bind:value={quotaSize} type="number" min="0" /> + {#if quotaSizeWarning} + <HelperText color="danger">{$t('errors.quota_higher_than_disk_size')}</HelperText> + {/if} + </Field> + </Stack> + + {#snippet stickyBottom()} + <Button color="secondary" fullWidth onclick={onCancel} shape="round">{$t('cancel')}</Button> + <Button type="submit" disabled={!valid} fullWidth shape="round">{$t('create')}</Button> + {/snippet} + </FullScreenModal> +</form> diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte index 3a61a1671c..78ff67cfcb 100644 --- a/web/src/lib/components/layouts/AuthPageLayout.svelte +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -11,7 +11,7 @@ </script> <section class="min-w-screen flex min-h-screen items-center justify-center"> - <Card color="secondary" class="w-full max-w-xl border m-2"> + <Card color="secondary" class="w-full max-w-lg border m-2"> <CardHeader class="mt-6"> <VStack> <Logo variant="icon" size="giant" /> diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte index de34092658..c367d001f2 100644 --- a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte @@ -17,7 +17,7 @@ </script> <script lang="ts"> - import Button from '$lib/components/elements/buttons/button.svelte'; + import { Button } from '@immich/ui'; import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk'; import SearchPeopleSection from './search-people-section.svelte'; import SearchLocationSection from './search-location-section.svelte'; @@ -163,7 +163,7 @@ </form> {#snippet stickyBottom()} - <Button type="reset" color="gray" fullwidth form={formId}>{$t('clear_all')}</Button> - <Button type="submit" fullwidth form={formId}>{$t('search')}</Button> + <Button shape="round" size="large" type="reset" color="secondary" fullWidth form={formId}>{$t('clear_all')}</Button> + <Button shape="round" size="large" type="submit" fullWidth form={formId}>{$t('search')}</Button> {/snippet} </FullScreenModal> diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index d93a8a5731..0ce8a1d018 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -2,9 +2,6 @@ import { page } from '$app/stores'; import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialogue.svelte'; import RestoreDialogue from '$lib/components/admin-page/restore-dialogue.svelte'; - import Button from '$lib/components/elements/buttons/button.svelte'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import Icon from '$lib/components/elements/icon.svelte'; import CreateUserForm from '$lib/components/forms/create-user-form.svelte'; import EditUserForm from '$lib/components/forms/edit-user-form.svelte'; @@ -15,12 +12,13 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import { locale } from '$lib/stores/preferences.store'; - import { serverConfig, featureFlags } from '$lib/stores/server-config.store'; + import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; import { copyToClipboard } from '$lib/utils'; import { getByteUnitString } from '$lib/utils/byte-units'; import { UserStatus, searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk'; + import { Button, Code, IconButton, Text } from '@immich/ui'; import { mdiContentCopy, mdiDeleteRestore, mdiInfinity, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; import { DateTime } from 'luxon'; import { onMount } from 'svelte'; @@ -161,22 +159,21 @@ > {#snippet promptSnippet()} <div class="flex flex-col gap-4"> - <p>{$t('admin.user_password_has_been_reset')}</p> + <Text>{$t('admin.user_password_has_been_reset')}</Text> - <div class="flex justify-center gap-2"> - <code - class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700" - > - {newPassword} - </code> - <LinkButton onclick={() => copyToClipboard(newPassword)} title={$t('copy_password')}> - <div class="flex place-items-center gap-2 text-sm"> - <Icon path={mdiContentCopy} size="18" /> - </div> - </LinkButton> + <div class="flex justify-center gap-2 items-center"> + <Code color="primary">{newPassword}</Code> + <IconButton + icon={mdiContentCopy} + shape="round" + color="secondary" + variant="ghost" + onclick={() => copyToClipboard(newPassword)} + title={$t('copy_password')} + /> </div> - <p>{$t('admin.user_password_reset_description')}</p> + <Text>{$t('admin.user_password_reset_description')}</Text> </div> {/snippet} </ConfirmDialog> @@ -222,31 +219,31 @@ class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all text-sm" > {#if !immichUser.deletedAt} - <CircleIconButton + <IconButton + shape="round" + size="large" icon={mdiPencilOutline} title={$t('edit_user')} - color="primary" - size="16" onclick={() => editUserHandler(immichUser)} /> {#if immichUser.id !== $user.id} - <CircleIconButton + <IconButton + shape="round" + size="large" icon={mdiTrashCanOutline} title={$t('delete_user')} - color="primary" - size="16" onclick={() => deleteUserHandler(immichUser)} /> {/if} {/if} {#if immichUser.deletedAt && immichUser.status === UserStatus.Deleted} - <CircleIconButton + <IconButton + shape="round" + size="large" icon={mdiDeleteRestore} title={$t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(immichUser.deletedAt) }, })} - color="primary" - size="16" onclick={() => restoreUserHandler(immichUser)} /> {/if} @@ -256,8 +253,7 @@ {/if} </tbody> </table> - - <Button size="sm" onclick={() => (shouldShowCreateUserForm = true)}>{$t('create_user')}</Button> + <Button shape="round" onclick={() => (shouldShowCreateUserForm = true)}>{$t('create_user')}</Button> </section> </section> </UserPageLayout> diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte index 6b91118475..a99e70bacb 100644 --- a/web/src/routes/auth/change-password/+page.svelte +++ b/web/src/routes/auth/change-password/+page.svelte @@ -51,9 +51,7 @@ <Field label={$t('confirm_password')} required> <PasswordInput bind:value={passwordConfirm} autocomplete="new-password" /> - {#if errorMessage} - <HelperText color="danger">{errorMessage}</HelperText> - {/if} + <HelperText color="danger">{errorMessage}</HelperText> </Field> <div class="my-5 flex w-full"> diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index f52face78e..bc062292fc 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -131,7 +131,7 @@ <div class="inline-flex w-full items-center justify-center mt-4"> <hr class="my-4 h-px w-3/4 border-0 bg-gray-200 dark:bg-gray-600" /> <span - class="absolute left-1/2 -translate-x-1/2 bg-white px-3 font-medium text-gray-900 dark:bg-immich-dark-gray dark:text-white" + class="absolute left-1/2 -translate-x-1/2 bg-gray-50 px-3 font-medium text-gray-900 dark:bg-neutral-900 dark:text-white" > {$t('or').toUpperCase()} </span> diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 12bfd7c604..2d81c28dd0 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -2,7 +2,11 @@ import plugin from 'tailwindcss/plugin'; /** @type {import('tailwindcss').Config} */ export default { - content: ['./src/**/*.{html,js,svelte,ts}', './node_modules/@immich/ui/dist/**/*.{svelte,js}'], + content: [ + './src/**/*.{html,js,svelte,ts}', + './node_modules/@immich/ui/dist/**/*.{svelte,js}', + '../../ui/src/**/*.{html,js,svelte,ts}', + ], darkMode: 'class', theme: { extend: {