feat: password change flow improvement, design improvements, css cleanup, move to shadcn canary

This commit is contained in:
Aidan 2025-04-10 21:15:38 -04:00
parent 916a4757aa
commit 8af9d140da
6 changed files with 233 additions and 178 deletions

View File

@ -1,71 +1,71 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme { @theme {
--color-border: hsl(var(--border)); --color-background: oklch(var(--background));
--color-input: hsl(var(--input)); --color-foreground: oklch(var(--foreground));
--color-ring: hsl(var(--ring)); --color-card: oklch(var(--card));
--color-background: hsl(var(--background)); --color-card-foreground: oklch(var(--card-foreground));
--color-foreground: hsl(var(--foreground)); --color-popover: oklch(var(--popover));
--color-popover-foreground: oklch(var(--popover-foreground));
--color-primary: oklch(var(--primary));
--color-primary-foreground: oklch(var(--primary-foreground));
--color-secondary: oklch(var(--secondary));
--color-secondary-foreground: oklch(var(--secondary-foreground));
--color-muted: oklch(var(--muted));
--color-muted-foreground: oklch(var(--muted-foreground));
--color-accent: oklch(var(--accent));
--color-accent-foreground: oklch(var(--accent-foreground));
--color-destructive: oklch(var(--destructive));
--color-destructive-foreground: oklch(var(--destructive-foreground));
--color-border: oklch(var(--border));
--color-input: oklch(var(--input));
--color-ring: oklch(var(--ring));
--color-primary: hsl(var(--primary)); /* Sidebar */
--color-primary-foreground: hsl(var(--primary-foreground)); --color-sidebar: oklch(var(--sidebar));
--color-sidebar-foreground: oklch(var(--sidebar-foreground));
--color-sidebar-primary: oklch(var(--sidebar-primary));
--color-sidebar-primary-foreground: oklch(var(--sidebar-primary-foreground));
--color-sidebar-accent: oklch(var(--sidebar-accent));
--color-sidebar-accent-foreground: oklch(var(--sidebar-accent-foreground));
--color-sidebar-border: oklch(var(--sidebar-border));
--color-sidebar-ring: oklch(var(--sidebar-ring));
--color-secondary: hsl(var(--secondary)); /* Chart */
--color-secondary-foreground: hsl(var(--secondary-foreground)); --color-chart-1: oklch(var(--chart-1));
--color-chart-2: oklch(var(--chart-2));
--color-chart-3: oklch(var(--chart-3));
--color-chart-4: oklch(var(--chart-4));
--color-chart-5: oklch(var(--chart-5));
--color-destructive: hsl(var(--destructive)); /* Border radius */
--color-destructive-foreground: hsl(var(--destructive-foreground)); --radius: 0.625rem;
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-sidebar: hsl(var(--sidebar-background));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-primary: hsl(var(--sidebar-primary));
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-ring));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--font-sans: /* Typography */
var(--font-sans), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', --font-sans: var(--font-sans), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
/* Animations */
--animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out;
}
@keyframes accordion-down { /* Keyframes */
from { @keyframes accordion-down {
height: 0; from { height: 0; }
} to { height: var(--radix-accordion-content-height); }
to { }
height: var(--radix-accordion-content-height);
} @keyframes accordion-up {
} from { height: var(--radix-accordion-content-height); }
@keyframes accordion-up { to { height: 0; }
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
} }
@utility container { @utility container {
@ -79,14 +79,6 @@
} }
} }
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base { @layer base {
*, *,
::after, ::after,
@ -95,103 +87,90 @@
::file-selector-button { ::file-selector-button {
border-color: var(--color-gray-200, currentColor); border-color: var(--color-gray-200, currentColor);
} }
}
@layer base {
:root { :root {
--background: 30 20% 98%; --background: 1 0 0;
--foreground: 222.2 84% 4.9%; --foreground: 0.129 0.042 264.695;
--card: 1 0 0;
--muted: 210 40% 96.1%; --card-foreground: 0.129 0.042 264.695;
--muted-foreground: 215.4 16.3% 46.9%; --popover: 1 0 0;
--popover-foreground: 0.129 0.042 264.695;
--popover: 0 0% 100%; --primary: 0.208 0.042 265.755;
--popover-foreground: 222.2 84% 4.9%; --primary-foreground: 0.984 0.003 247.858;
--secondary: 0.968 0.007 247.896;
--card: 0 0% 100%; --secondary-foreground: 0.208 0.042 265.755;
--card-foreground: 222.2 84% 4.9%; --muted: 0.968 0.007 247.896;
--muted-foreground: 0.554 0.046 257.417;
--border: 214.3 31.8% 91.4%; --accent: 0.968 0.007 247.896;
--input: 214.3 31.8% 91.4%; --accent-foreground: 0.208 0.042 265.755;
--destructive: 0.577 0.245 27.325;
--primary: 222.2 47.4% 11.2%; --destructive-foreground: 0.984 0.003 247.858;
--primary-foreground: 210 40% 98%; --border: 0.929 0.013 255.508;
--input: 0.929 0.013 255.508;
--secondary: 210 40% 96.1%; --ring: 0.704 0.04 256.788;
--secondary-foreground: 222.2 47.4% 11.2%;
/* Chart */
--accent: 210 60% 95%; --chart-1: 0.646 0.222 41.116;
--accent-foreground: 222.2 47.4% 11.2%; --chart-2: 0.6 0.118 184.704;
--chart-3: 0.398 0.07 227.392;
--destructive: 0 84.2% 60.2%; --chart-4: 0.828 0.189 84.429;
--destructive-foreground: 210 40% 98%; --chart-5: 0.769 0.188 70.08;
--ring: 215 20.2% 65.1%; /* Sidebar */
--sidebar: 0.984 0.003 247.858;
--radius: 0.5rem; --sidebar-foreground: 0.129 0.042 264.695;
--sidebar-primary: 0.208 0.042 265.755;
--sidebar-background: 215 25% 97%; --sidebar-primary-foreground: 0.984 0.003 247.858;
--sidebar-foreground: 240 5.3% 26.1%; --sidebar-accent: 0.968 0.007 247.896;
--sidebar-primary: 240 5.9% 10%; --sidebar-accent-foreground: 0.208 0.042 265.755;
--sidebar-primary-foreground: 0 0% 98%; --sidebar-border: 0.929 0.013 255.508;
--sidebar-accent: 215 10% 92%; --sidebar-ring: 0.704 0.04 256.788;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 88%;
--sidebar-ring: 217.2 91.2% 59.8%;
} }
/* Dark theme */
.dark { .dark {
--background: 222.2 84% 4.9%; --background: 0.129 0.042 264.695;
--foreground: 210 40% 98%; --foreground: 0.984 0.003 247.858;
--card: 0.208 0.042 265.755;
--muted: 217.2 32.6% 17.5%; --card-foreground: 0.984 0.003 247.858;
--muted-foreground: 215 20.2% 65.1%; --popover: 0.208 0.042 265.755;
--popover-foreground: 0.984 0.003 247.858;
--popover: 222.2 84% 4.9%; --primary: 0.929 0.013 255.508;
--popover-foreground: 210 40% 98%; --primary-foreground: 0.208 0.042 265.755;
--secondary: 0.279 0.041 260.031;
--card: 222.2 84% 4.9%; --secondary-foreground: 0.984 0.003 247.858;
--card-foreground: 210 40% 98%; --muted: 0.279 0.041 260.031;
--muted-foreground: 0.704 0.04 256.788;
--border: 217.2 32.6% 17.5%; --accent: 0.279 0.041 260.031;
--input: 217.2 32.6% 17.5%; --accent-foreground: 0.984 0.003 247.858;
--destructive: 0.704 0.191 22.216;
--primary: 210 40% 98%; --destructive-foreground: 0.984 0.003 247.858;
--primary-foreground: 222.2 47.4% 11.2%; --border: 1 0 0 / 10%;
--input: 1 0 0 / 15%;
--secondary: 217.2 32.6% 17.5%; --ring: 0.551 0.027 264.364;
--secondary-foreground: 210 40% 98%;
/* Chart */
--accent: 217.2 32.6% 17.5%; --chart-1: 0.488 0.243 264.376;
--accent-foreground: 210 40% 98%; --chart-2: 0.696 0.17 162.48;
--chart-3: 0.769 0.188 70.08;
--destructive: 0 62.8% 30.6%; --chart-4: 0.627 0.265 303.9;
--destructive-foreground: 0 85.7% 97.3%; --chart-5: 0.645 0.246 16.439;
--ring: 217.2 32.6% 17.5%; /* Sidebar */
--sidebar: 0.208 0.042 265.755;
--sidebar-background: 240 5.9% 10%; --sidebar-foreground: 0.984 0.003 247.858;
--sidebar-primary: 0.488 0.243 264.376;
--sidebar-foreground: 240 4.8% 95.9%; --sidebar-primary-foreground: 0.984 0.003 247.858;
--sidebar-accent: 0.279 0.041 260.031;
--sidebar-primary: 224.3 76.3% 48%; --sidebar-accent-foreground: 0.984 0.003 247.858;
--sidebar-border: 1 0 0 / 10%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-ring: 0.551 0.027 264.364;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
} }
}
@layer base {
* { * {
@apply border-border; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }

View File

@ -4,7 +4,7 @@
"rsc": true, "rsc": true,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "tailwind.config.ts", "config": "",
"css": "app/globals.css", "css": "app/globals.css",
"baseColor": "slate", "baseColor": "slate",
"cssVariables": true, "cssVariables": true,

View File

@ -5,12 +5,23 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Mail, Key, Loader2 } from "lucide-react" import { Key, Loader2, User } from "lucide-react"
import Link from "next/link"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog"
export function ChangePassword() { export function ChangePassword() {
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null); const [message, setMessage] = useState<string | null>(null);
const [open, setOpen] = useState(false);
const handlePasswordChange = async (e: React.FormEvent<HTMLFormElement>) => { const handlePasswordChange = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
@ -29,6 +40,11 @@ export function ChangePassword() {
if (response.ok && resData.success) { if (response.ok && resData.success) {
setMessage("Password Updated"); setMessage("Password Updated");
setLoading(false); setLoading(false);
// Close dialog after change
setTimeout(() => {
setOpen(false);
setNewPassword("");
}, 1500);
} else if (resData.error) { } else if (resData.error) {
setMessage(resData.error); setMessage(resData.error);
setLoading(false); setLoading(false);
@ -46,28 +62,63 @@ export function ChangePassword() {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center"><Mail size={15} className="mr-1" /> Change Email Password</CardTitle> <CardTitle className="flex items-center text-2xl">
<CardDescription>Please note, this will <b>NOT</b> change your Authentik password.</CardDescription> <User className="mr-1" />
{/* TODO: please tell me you added password resets to authentik by now */} My Account
</CardTitle>
<CardDescription>LibreCloud makes it easy to manage your account</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handlePasswordChange} className="space-y-4"> <h2 className="text-lg font-bold">Actions</h2>
<div className="space-y-2"> <Dialog open={open} onOpenChange={setOpen}>
<Label htmlFor="new-password">New Password</Label> <DialogTrigger asChild>
<Input <Button className="mt-2">
id="new-password" <Key />
type="password" Change Password
value={newPassword} </Button>
onChange={(e) => setNewPassword(e.target.value)} </DialogTrigger>
className="mt-1.5" <DialogContent>
/> <DialogHeader>
</div> <DialogTitle>Change Your Password</DialogTitle>
<Button type="submit" disabled={loading || newPassword.length < 8}> <DialogDescription>
{/* TODO: this should have the usual error message style */ <span className="font-bold mr-1">This only applies to your Authentik account.</span>
loading ? <><Loader2 className="animate-spin" /> Changing...</> : <><Key /> Change Password</>} Make sure it's secure, and consider using
</Button> <Link
{message && <p className="text-sm text-center">{message}</p>} href="https://pass.librecloud.cc"
</form> target="_blank"
className="ml-1 underline hover:text-primary transition-all"
>
LibreCloud Pass
</Link> to keep it safe.
</DialogDescription>
</DialogHeader>
<form onSubmit={handlePasswordChange} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="new-password">New Password</Label>
<Input
id="new-password"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="mt-1.5"
/>
<p className="text-xs text-muted-foreground">
Password must be at least 8 characters long.
</p>
</div>
{message && (
<p className={`text-sm text-center ${message.includes("Updated") ? "text-green-500" : "text-red-500"}`}>
{message}
</p>
)}
<DialogFooter>
<Button type="submit" disabled={loading || newPassword.length < 8}>
{loading ? <><Loader2 className="animate-spin mr-2" /> Changing...</> : <><Key className="mr-2" /> Change Password</>}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@ -98,8 +98,9 @@ export function LinkGitea({ linked }: { linked: boolean }) {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle className="flex items-center text-2xl">
Link Gitea Account <SiGitea className="mr-2" />
Gitea Link
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
To link your Gitea account to your LibreCloud account, add your p0ntus mail account to your Gitea account, then click the button. To link your Gitea account to your LibreCloud account, add your p0ntus mail account to your Gitea account, then click the button.
@ -164,7 +165,10 @@ export function LinkGitea({ linked }: { linked: boolean }) {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Git Link</CardTitle> <CardTitle className="flex items-center text-2xl">
<SiGitea className="mr-2" />
Gitea Link
</CardTitle>
<CardDescription> <CardDescription>
Your Gitea account is currently linked to your LibreCloud account. Your Gitea account is currently linked to your LibreCloud account.
</CardDescription> </CardDescription>
@ -178,7 +182,12 @@ export function LinkGitea({ linked }: { linked: boolean }) {
</Alert> </Alert>
)} )}
<p className="text-sm mb-4"> <p className="text-sm mb-4">
Unlinking your Gitea account will not delete your Gitea account. You can delete your Gitea account <Link href="https://try.gitea.com/user/sign_up" target="_blank" className="underline hover:text-muted-foreground">here</Link>. Unlinking your Gitea account will not delete your Gitea account. You can delete your Gitea account
<Link
href="https://git.pontusmail.org/user/settings/account"
target="_blank"
className="underline hover:text-muted-foreground transition-all ml-1"
>here</Link>.
</p> </p>
{unlinkLoading ? ( {unlinkLoading ? (
<Button variant="destructive" disabled> <Button variant="destructive" disabled>

View File

@ -3,7 +3,8 @@ import { Button } from "@/components/ui/button"
import { import {
Mail, Mail,
Headset, Headset,
Heart Heart,
Scale
} from "lucide-react" } from "lucide-react"
import Link from "next/link" import Link from "next/link"
@ -26,6 +27,7 @@ export const QuickLinks = () => {
</Button> </Button>
</Link> </Link>
<Link <Link
target="_blank"
href="https://mail.librecloud.cc" href="https://mail.librecloud.cc"
> >
<Button <Button
@ -37,6 +39,7 @@ export const QuickLinks = () => {
</Button> </Button>
</Link> </Link>
<Link <Link
target="_blank"
href={process.env.NEXT_PUBLIC_DONATE_URL || "https://donate.stripe.com/6oE8yxaPk6yXbpS145"} href={process.env.NEXT_PUBLIC_DONATE_URL || "https://donate.stripe.com/6oE8yxaPk6yXbpS145"}
> >
<Button <Button
@ -47,6 +50,18 @@ export const QuickLinks = () => {
Donate Donate
</Button> </Button>
</Link> </Link>
<Link
target="_blank"
href="/legal"
>
<Button
variant="secondary"
className="w-full mb-2 cursor-pointer"
>
<Scale />
Legal
</Button>
</Link>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@ -32,7 +32,7 @@
"framer-motion": "^12.6.3", "framer-motion": "^12.6.3",
"geist": "^1.3.1", "geist": "^1.3.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.474.0", "lucide-react": "^0.487.0",
"motion": "^12.6.3", "motion": "^12.6.3",
"next": "^15.3.0", "next": "^15.3.0",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
@ -45,7 +45,8 @@
"react-hook-form": "^7.55.0", "react-hook-form": "^7.55.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-typed": "^2.0.12", "react-typed": "^2.0.12",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^3.2.0",
"tw-animate-css": "^1.2.5",
"validator": "^13.15.0", "validator": "^13.15.0",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },