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 */
@keyframes accordion-down { @keyframes accordion-down {
from { from { height: 0; }
height: 0; to { height: var(--radix-accordion-content-height); }
}
to {
height: var(--radix-accordion-content-height);
}
} }
@keyframes accordion-up { @keyframes accordion-up {
from { from { height: var(--radix-accordion-content-height); }
height: var(--radix-accordion-content-height); to { height: 0; }
}
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;
--card-foreground: 0.129 0.042 264.695;
--popover: 1 0 0;
--popover-foreground: 0.129 0.042 264.695;
--primary: 0.208 0.042 265.755;
--primary-foreground: 0.984 0.003 247.858;
--secondary: 0.968 0.007 247.896;
--secondary-foreground: 0.208 0.042 265.755;
--muted: 0.968 0.007 247.896;
--muted-foreground: 0.554 0.046 257.417;
--accent: 0.968 0.007 247.896;
--accent-foreground: 0.208 0.042 265.755;
--destructive: 0.577 0.245 27.325;
--destructive-foreground: 0.984 0.003 247.858;
--border: 0.929 0.013 255.508;
--input: 0.929 0.013 255.508;
--ring: 0.704 0.04 256.788;
--muted: 210 40% 96.1%; /* Chart */
--muted-foreground: 215.4 16.3% 46.9%; --chart-1: 0.646 0.222 41.116;
--chart-2: 0.6 0.118 184.704;
--chart-3: 0.398 0.07 227.392;
--chart-4: 0.828 0.189 84.429;
--chart-5: 0.769 0.188 70.08;
--popover: 0 0% 100%; /* Sidebar */
--popover-foreground: 222.2 84% 4.9%; --sidebar: 0.984 0.003 247.858;
--sidebar-foreground: 0.129 0.042 264.695;
--card: 0 0% 100%; --sidebar-primary: 0.208 0.042 265.755;
--card-foreground: 222.2 84% 4.9%; --sidebar-primary-foreground: 0.984 0.003 247.858;
--sidebar-accent: 0.968 0.007 247.896;
--border: 214.3 31.8% 91.4%; --sidebar-accent-foreground: 0.208 0.042 265.755;
--input: 214.3 31.8% 91.4%; --sidebar-border: 0.929 0.013 255.508;
--sidebar-ring: 0.704 0.04 256.788;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 60% 95%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
--sidebar-background: 215 25% 97%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 215 10% 92%;
--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;
--card-foreground: 0.984 0.003 247.858;
--popover: 0.208 0.042 265.755;
--popover-foreground: 0.984 0.003 247.858;
--primary: 0.929 0.013 255.508;
--primary-foreground: 0.208 0.042 265.755;
--secondary: 0.279 0.041 260.031;
--secondary-foreground: 0.984 0.003 247.858;
--muted: 0.279 0.041 260.031;
--muted-foreground: 0.704 0.04 256.788;
--accent: 0.279 0.041 260.031;
--accent-foreground: 0.984 0.003 247.858;
--destructive: 0.704 0.191 22.216;
--destructive-foreground: 0.984 0.003 247.858;
--border: 1 0 0 / 10%;
--input: 1 0 0 / 15%;
--ring: 0.551 0.027 264.364;
--muted: 217.2 32.6% 17.5%; /* Chart */
--muted-foreground: 215 20.2% 65.1%; --chart-1: 0.488 0.243 264.376;
--chart-2: 0.696 0.17 162.48;
--chart-3: 0.769 0.188 70.08;
--chart-4: 0.627 0.265 303.9;
--chart-5: 0.645 0.246 16.439;
--popover: 222.2 84% 4.9%; /* Sidebar */
--popover-foreground: 210 40% 98%; --sidebar: 0.208 0.042 265.755;
--sidebar-foreground: 0.984 0.003 247.858;
--card: 222.2 84% 4.9%; --sidebar-primary: 0.488 0.243 264.376;
--card-foreground: 210 40% 98%; --sidebar-primary-foreground: 0.984 0.003 247.858;
--sidebar-accent: 0.279 0.041 260.031;
--border: 217.2 32.6% 17.5%; --sidebar-accent-foreground: 0.984 0.003 247.858;
--input: 217.2 32.6% 17.5%; --sidebar-border: 1 0 0 / 10%;
--sidebar-ring: 0.551 0.027 264.364;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--ring: 217.2 32.6% 17.5%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--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,11 +62,36 @@ 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>
<h2 className="text-lg font-bold">Actions</h2>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="mt-2">
<Key />
Change Password
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Change Your Password</DialogTitle>
<DialogDescription>
<span className="font-bold mr-1">This only applies to your Authentik account.</span>
Make sure it's secure, and consider using
<Link
href="https://pass.librecloud.cc"
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"> <form onSubmit={handlePasswordChange} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="new-password">New Password</Label> <Label htmlFor="new-password">New Password</Label>
@ -61,13 +102,23 @@ export function ChangePassword() {
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
className="mt-1.5" className="mt-1.5"
/> />
<p className="text-xs text-muted-foreground">
Password must be at least 8 characters long.
</p>
</div> </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}> <Button type="submit" disabled={loading || newPassword.length < 8}>
{/* TODO: this should have the usual error message style */ {loading ? <><Loader2 className="animate-spin mr-2" /> Changing...</> : <><Key className="mr-2" /> Change Password</>}
loading ? <><Loader2 className="animate-spin" /> Changing...</> : <><Key /> Change Password</>}
</Button> </Button>
{message && <p className="text-sm text-center">{message}</p>} </DialogFooter>
</form> </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"
}, },