Compare commits

...

3 Commits

Author SHA1 Message Date
Henry
1ff9353e20 feat: wrap title onto new line 2025-11-21 21:31:05 +00:00
Henry
8907362c58 feat: add card breadcrumb navigation 2025-11-20 22:19:22 +00:00
Henry
1651faae14 fix(cloud): reenable magic login on cloud and extend session duration (#248)
* fix(cloud): reenable magic login

* feat: extend session duration
2025-11-20 00:01:23 +00:00
5 changed files with 96 additions and 50 deletions

View File

@@ -303,6 +303,10 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
const password = watch("password"); const password = watch("password");
const isMagicLinkAvailable = useMemo(() => {
return isCloudEnv || (isEmailSendingEnabled && !isSignUp);
}, [isCloudEnv, isEmailSendingEnabled, isSignUp]);
// Determine if we should operate in magic link mode for current form state (login only) // Determine if we should operate in magic link mode for current form state (login only)
const isMagicLinkMode = useMemo(() => { const isMagicLinkMode = useMemo(() => {
// Magic link only viable when email sending enabled AND not sign up. // Magic link only viable when email sending enabled AND not sign up.
@@ -390,7 +394,7 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
)} )}
</div> </div>
)} )}
{isCredentialsEnabled && ( {(isCredentialsEnabled || isMagicLinkAvailable) && (
<> <>
<div> <div>
<Input <Input
@@ -404,26 +408,28 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
)} )}
</div> </div>
<div> {isCredentialsEnabled && (
<Input <div>
type="password" <Input
{...register("password", { required: true })} type="password"
placeholder={t`Enter your password`} {...register("password", { required: true })}
/> placeholder={t`Enter your password`}
{errors.password && ( />
<p className="mt-2 text-xs text-red-400"> {errors.password && (
{errors.password.message ?? <p className="mt-2 text-xs text-red-400">
t`Please enter a valid password`} {errors.password.message ??
</p> t`Please enter a valid password`}
)} </p>
</div> )}
</div>
)}
</> </>
)} )}
{loginError && ( {loginError && (
<p className="mt-2 text-xs text-red-400">{loginError}</p> <p className="mt-2 text-xs text-red-400">{loginError}</p>
)} )}
</div> </div>
{isCredentialsEnabled && ( {(isCredentialsEnabled || isMagicLinkAvailable) && (
<div className="mt-[1.5rem] flex items-center gap-4"> <div className="mt-[1.5rem] flex items-center gap-4">
<Button <Button
isLoading={isLoginWithEmailPending} isLoading={isLoginWithEmailPending}

View File

@@ -122,10 +122,10 @@ export default function SideNavigation({
)} )}
> >
<div> <div>
<div className="hidden h-14 items-center justify-between pb-[18px] pt-1.5 md:flex"> <div className="hidden h-[45px] items-center justify-between pb-3 md:flex">
{!isCollapsed && ( {!isCollapsed && (
<Link href="/" className="block"> <Link href="/" className="block">
<h1 className="pl-2 text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000"> <h1 className="pl-2 text-[16px] font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
kan.bn kan.bn
</h1> </h1>
</Link> </Link>

View File

@@ -28,7 +28,7 @@ export default function BoardDropdown() {
}, },
]} ]}
> >
<HiEllipsisHorizontal className="h-6 w-6 text-dark-900" /> <HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
</Dropdown> </Dropdown>
); );
} }

View File

@@ -107,7 +107,7 @@ export function CardRightPanel({ isTemplate }: { isTemplate?: boolean }) {
return ( return (
<div className="h-full w-[360px] border-l-[1px] border-light-300 bg-light-50 p-8 text-light-900 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900"> <div className="h-full w-[360px] border-l-[1px] border-light-300 bg-light-50 p-8 text-light-900 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
<div className="mb-4 flex w-full flex-row"> <div className="mb-4 flex w-full flex-row pt-[18px]">
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`List`}</p> <p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`List`}</p>
<ListSelector <ListSelector
cardPublicId={cardId ?? ""} cardPublicId={cardId ?? ""}
@@ -210,6 +210,17 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
} }
}, [card, getModalState, clearModalState]); }, [card, getModalState, clearModalState]);
// Auto-resize title textarea
useEffect(() => {
const titleTextarea = document.getElementById(
"title",
) as HTMLTextAreaElement;
if (titleTextarea) {
titleTextarea.style.height = "auto";
titleTextarea.style.height = `${titleTextarea.scrollHeight}px`;
}
}, [card]);
if (!cardId) return <></>; if (!cardId) return <></>;
return ( return (
@@ -217,47 +228,71 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
<PageHead <PageHead
title={t`${card?.title ?? t`Card`} | ${board?.name ?? t`Board`}`} title={t`${card?.title ?? t`Card`} | ${board?.name ?? t`Board`}`}
/> />
<div className="flex h-full flex-1 flex-row overflow-hidden"> <div className="flex h-full flex-1 flex-col overflow-hidden">
{/* Full-width top strip with board link and dropdown */}
<div className="flex w-full items-center justify-between border-b-[1px] border-light-300 bg-light-50 px-8 py-2 dark:border-dark-300 dark:bg-dark-50">
{!card && isLoading && (
<div className="flex space-x-2">
<div className="h-[1.5rem] w-[150px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
</div>
)}
{card && (
<>
<div className="flex items-center gap-1">
<Link
className="whitespace-nowrapleading-[1.5rem] text-sm font-bold text-light-900 dark:text-dark-950"
href={`${isTemplate ? "/templates" : "/boards"}`}
>
{workspace.name}
</Link>
<IoChevronForwardSharp className="h-[10px] w-[10px] text-light-900 dark:text-dark-900" />
<Link
className="whitespace-nowrap text-sm font-bold leading-[1.5rem] text-light-900 dark:text-dark-950"
href={`${isTemplate ? "/templates" : "/boards"}/${board?.publicId}`}
>
{board?.name}
</Link>
</div>
<div className="flex items-center gap-2">
<Dropdown />
</div>
</>
)}
{!card && !isLoading && (
<p className="block p-0 py-0 font-bold leading-[1.5rem] tracking-tight text-light-900 dark:text-dark-900 sm:text-[1rem]">
{t`Card not found`}
</p>
)}
</div>
<div className="scrollbar-thumb-rounded-[4px] scrollbar-track-rounded-[4px] w-full flex-1 overflow-y-auto scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 hover:scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300 dark:hover:scrollbar-thumb-dark-300"> <div className="scrollbar-thumb-rounded-[4px] scrollbar-track-rounded-[4px] w-full flex-1 overflow-y-auto scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 hover:scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300 dark:hover:scrollbar-thumb-dark-300">
<div className="p-auto mx-auto flex h-full w-full max-w-[800px] flex-col"> <div className="p-auto mx-auto flex h-full w-full max-w-[800px] flex-col">
<div className="p-6 md:p-8"> <div className="p-6 md:p-8">
<div className="mb-8 flex w-full items-center justify-between md:mt-6"> <div className="mb-8 md:mt-4">
{!card && isLoading && ( {!card && isLoading && (
<div className="flex space-x-2"> <div className="flex space-x-2">
<div className="h-[2.3rem] w-[150px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
<div className="h-[2.3rem] w-[300px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" /> <div className="h-[2.3rem] w-[300px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
</div> </div>
)} )}
{card && ( {card && (
<> <form
<Link onSubmit={handleSubmit(onSubmit)}
className="whitespace-nowrap font-bold leading-[2.3rem] tracking-tight text-light-900 dark:text-dark-900 sm:text-[1.2rem]" className="w-full space-y-6"
href={`${isTemplate ? "/templates" : "/boards"}/${board?.publicId}`} >
> <div>
{board?.name} <textarea
</Link> id="title"
<IoChevronForwardSharp {...register("title")}
size={18} onBlur={handleSubmit(onSubmit)}
className="mx-2 text-light-900 dark:text-dark-900" rows={1}
/> className="block w-full resize-none overflow-hidden border-0 bg-transparent p-0 py-0 font-bold leading-relaxed text-neutral-900 focus:ring-0 dark:text-dark-1000 sm:text-[1.2rem]"
<form onInput={(e) => {
onSubmit={handleSubmit(onSubmit)} const target = e.target as HTMLTextAreaElement;
className="w-full space-y-6" target.style.height = "auto";
> target.style.height = `${target.scrollHeight}px`;
<div> }}
<input />
type="text"
id="title"
{...register("title")}
onBlur={handleSubmit(onSubmit)}
className="block w-full border-0 bg-transparent p-0 py-0 font-bold tracking-tight text-neutral-900 focus:ring-0 dark:text-dark-1000 sm:text-[1.2rem]"
/>
</div>
</form>
<div className="flex">
<Dropdown />
</div> </div>
</> </form>
)} )}
{!card && !isLoading && ( {!card && !isLoading && (
<p className="block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]"> <p className="block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">

View File

@@ -129,6 +129,11 @@ export const initAuth = (db: dbClient) => {
user: schema.users, user: schema.users,
}, },
}), }),
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 days
updateAge: 60 * 60 * 24 * 2, // Update session expiry every 48 hours if user is active
freshAge: 0,
},
emailAndPassword: { emailAndPassword: {
enabled: env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true", enabled: env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true",
disableSignUp: disableSignUp: