this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(settings): working

+157 -48
+1
package.json
··· 14 14 "@hugeicons/core-free-icons": "^3.1.1", 15 15 "@hugeicons/react": "^1.1.5", 16 16 "@neondatabase/serverless": "^1.0.2", 17 + "@vercel/blob": "^2.2.0", 17 18 "better-auth": "^1.4.18", 18 19 "class-variance-authority": "^0.7.1", 19 20 "clsx": "^2.1.1",
+46
pnpm-lock.yaml
··· 20 20 '@neondatabase/serverless': 21 21 specifier: ^1.0.2 22 22 version: 1.0.2 23 + '@vercel/blob': 24 + specifier: ^2.2.0 25 + version: 2.2.0 23 26 better-auth: 24 27 specifier: ^1.4.18 25 28 version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@neondatabase/serverless@1.0.2)(@types/pg@8.16.0)(kysely@0.28.11))(next@16.1.6(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ··· 1259 1262 '@types/validate-npm-package-name@4.0.2': 1260 1263 resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} 1261 1264 1265 + '@vercel/blob@2.2.0': 1266 + resolution: {integrity: sha512-h9ruqqTyAlLrmLIT55NUDnfc889feZnQXWOBxpXrAiAzWKmHqIokLchuMBKFsT9MDV/jM0HRQlvlTyubMHrOKA==} 1267 + engines: {node: '>=20.0.0'} 1268 + 1262 1269 accepts@2.0.0: 1263 1270 resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} 1264 1271 engines: {node: '>= 0.6'} ··· 1300 1307 ast-types@0.16.1: 1301 1308 resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} 1302 1309 engines: {node: '>=4'} 1310 + 1311 + async-retry@1.3.3: 1312 + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} 1303 1313 1304 1314 babel-plugin-react-compiler@1.0.0: 1305 1315 resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} ··· 1949 1959 is-arrayish@0.2.1: 1950 1960 resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} 1951 1961 1962 + is-buffer@2.0.5: 1963 + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} 1964 + engines: {node: '>=4'} 1965 + 1952 1966 is-docker@3.0.0: 1953 1967 resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} 1954 1968 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} ··· 2480 2494 resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} 2481 2495 engines: {node: '>=18'} 2482 2496 2497 + retry@0.13.1: 2498 + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} 2499 + engines: {node: '>= 4'} 2500 + 2483 2501 rettime@0.10.1: 2484 2502 resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} 2485 2503 ··· 2656 2674 resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} 2657 2675 engines: {node: '>=6'} 2658 2676 2677 + throttleit@2.1.0: 2678 + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} 2679 + engines: {node: '>=18'} 2680 + 2659 2681 tiny-invariant@1.3.3: 2660 2682 resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} 2661 2683 ··· 2719 2741 undici-types@7.16.0: 2720 2742 resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 2721 2743 2744 + undici@6.23.0: 2745 + resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} 2746 + engines: {node: '>=18.17'} 2747 + 2722 2748 unicorn-magic@0.3.0: 2723 2749 resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} 2724 2750 engines: {node: '>=18'} ··· 3741 3767 3742 3768 '@types/validate-npm-package-name@4.0.2': {} 3743 3769 3770 + '@vercel/blob@2.2.0': 3771 + dependencies: 3772 + async-retry: 1.3.3 3773 + is-buffer: 2.0.5 3774 + is-node-process: 1.2.0 3775 + throttleit: 2.1.0 3776 + undici: 6.23.0 3777 + 3744 3778 accepts@2.0.0: 3745 3779 dependencies: 3746 3780 mime-types: 3.0.2 ··· 3775 3809 dependencies: 3776 3810 tslib: 2.8.1 3777 3811 3812 + async-retry@1.3.3: 3813 + dependencies: 3814 + retry: 0.13.1 3815 + 3778 3816 babel-plugin-react-compiler@1.0.0: 3779 3817 dependencies: 3780 3818 '@babel/types': 7.29.0 ··· 4348 4386 4349 4387 is-arrayish@0.2.1: {} 4350 4388 4389 + is-buffer@2.0.5: {} 4390 + 4351 4391 is-docker@3.0.0: {} 4352 4392 4353 4393 is-extglob@2.1.1: {} ··· 4787 4827 onetime: 7.0.0 4788 4828 signal-exit: 4.1.0 4789 4829 4830 + retry@0.13.1: {} 4831 + 4790 4832 rettime@0.10.1: {} 4791 4833 4792 4834 reusify@1.1.0: {} ··· 5027 5069 5028 5070 tapable@2.3.0: {} 5029 5071 5072 + throttleit@2.1.0: {} 5073 + 5030 5074 tiny-invariant@1.3.3: {} 5031 5075 5032 5076 tinyexec@1.0.2: {} ··· 5084 5128 undici-types@6.21.0: {} 5085 5129 5086 5130 undici-types@7.16.0: {} 5131 + 5132 + undici@6.23.0: {} 5087 5133 5088 5134 unicorn-magic@0.3.0: {} 5089 5135
+70 -46
src/app/settings/page.tsx
··· 1 1 "use client"; 2 2 3 - import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 - import { Avatar, AvatarImage } from "@/components/ui/avatar"; 5 - import { Button } from "@/components/ui/button"; 6 3 import { ArrowLeft01Icon, Logout01Icon } from "@hugeicons/core-free-icons"; 7 4 import { HugeiconsIcon } from "@hugeicons/react"; 8 5 import Link from "next/link"; 9 - import { Field, FieldLabel } from "@/components/ui/field"; 6 + import { Button } from "@/components/ui/button"; 7 + import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 8 + import { Field, FieldGroup, FieldLabel,} from "@/components/ui/field"; 10 9 import { Input } from "@/components/ui/input"; 11 - import { Switch } from "@/components/ui/switch"; 10 + import { authClient } from "@/lib/auth-client"; 11 + import { uploadImage } from "@/lib/image"; 12 12 13 13 export default function Page() { 14 - const me = { 15 - id: 1, 16 - name: "Alice", 17 - avatar: "https://github.com/alice.png", 18 - email: "alice@example.com", 19 - notifications: true, 14 + const { data: session } = authClient.useSession(); 15 + const user = session?.user; 16 + 17 + if (!user) return null; 18 + 19 + const handleSave = async (e: React.SubmitEvent<HTMLFormElement>) => { 20 + e.preventDefault(); 21 + const formData = new FormData(e.currentTarget); 22 + 23 + const picture = formData.get("picture"); 24 + const name = formData.get("name"); 25 + const email = formData.get("email"); 26 + 27 + if (picture instanceof File && picture.size > 0) { 28 + const { url } = await uploadImage(picture); 29 + await authClient.updateUser({ 30 + image: url, 31 + }); 32 + } 33 + 34 + if (name && name !== user.name) 35 + await authClient.updateUser({ 36 + name: name as string, 37 + }); 38 + 39 + if (email && email !== user.email) 40 + await authClient.changeEmail({ 41 + newEmail: email as string, 42 + callbackURL: "/", 43 + }); 20 44 }; 21 45 22 46 return ( 23 - <> 24 - <header> 47 + <Card className="ring-0"> 48 + <CardHeader> 25 49 <Button variant="secondary" size="icon" render={<Link href="/" />}> 26 50 <HugeiconsIcon icon={ArrowLeft01Icon} /> 27 51 </Button> 28 - </header> 29 - <Card className="ring-0"> 30 - <CardHeader> 31 - <CardTitle>Settings</CardTitle> 32 - </CardHeader> 33 - <CardContent className="space-y-6"> 34 - <Avatar className="w-24 h-24"> 35 - <AvatarImage src={me.avatar} alt={me.name} /> 36 - </Avatar> 37 - <Field> 38 - <FieldLabel>Name</FieldLabel> 39 - <Input defaultValue={me.name} /> 40 - </Field> 41 - <Field> 42 - <FieldLabel>Email</FieldLabel> 43 - <Input defaultValue={me.email} /> 44 - </Field> 45 - <Field> 46 - <FieldLabel>Notifications</FieldLabel> 47 - <Switch id="notifications" defaultChecked={me.notifications} /> 48 - </Field> 49 - <Button className="w-full">Save Changes</Button> 50 - <Button 51 - variant="destructive" 52 - className="w-full" 53 - render={<Link href="/auth" />} 54 - > 55 - <HugeiconsIcon icon={Logout01Icon} className="me-2" /> 56 - Logout 57 - </Button> 58 - </CardContent> 59 - </Card> 60 - </> 52 + <CardTitle>Settings</CardTitle> 53 + </CardHeader> 54 + <CardContent> 55 + <form onSubmit={handleSave}> 56 + <FieldGroup> 57 + <Field> 58 + <FieldLabel htmlFor="picture">Picture</FieldLabel> 59 + <Input id="picture" type="file" name="picture" accept="image/*" /> 60 + </Field> 61 + <Field> 62 + <FieldLabel>Name</FieldLabel> 63 + <Input defaultValue={user.name} name="name" /> 64 + </Field> 65 + <Field> 66 + <FieldLabel>Email</FieldLabel> 67 + <Input defaultValue={user.email} name="email" /> 68 + </Field> 69 + <Button className="w-full" type="submit"> 70 + Save Changes 71 + </Button> 72 + <Button 73 + variant="destructive" 74 + className="w-full" 75 + render={<Link href="/auth" />} 76 + onClick={() => authClient.signOut()} 77 + > 78 + <HugeiconsIcon icon={Logout01Icon} className="me-2" /> 79 + Logout 80 + </Button> 81 + </FieldGroup> 82 + </form> 83 + </CardContent> 84 + </Card> 61 85 ); 62 86 }
+15 -2
src/lib/auth.ts
··· 11 11 provider: "pg", 12 12 schema, 13 13 }), 14 + user: { changeEmail: { enabled: true, } }, 15 + emailVerification: { 16 + sendVerificationEmail: async ({ user, url, token }) => { 17 + await sendEmail({ 18 + to: user.email, 19 + subject: "Verify your email for Vouch", 20 + html: ` 21 + <h1>Welcome to Vouch</h1> 22 + <p>Click the link below to verify your email address:</p> 23 + <a href="${url}">Verify Email</a> 24 + `, 25 + }); 26 + } 27 + }, 14 28 plugins: [ 15 29 magicLink({ 16 30 sendMagicLink: async ({ email, token, url }, request) => { ··· 21 35 <h1>Welcome to Vouch</h1> 22 36 <p>Click the link below to sign in:</p> 23 37 <a href="${url}">Sign in now</a> 24 - <p>This link expires in 5 minutes.</p> 25 38 `, 26 39 }); 27 40 }, 28 41 }), 29 - ], 42 + ] 30 43 });
+9
src/lib/image.ts
··· 1 + "use server"; 2 + 3 + import { put } from "@vercel/blob"; 4 + 5 + export async function uploadImage(file: File) { 6 + return put(file.name, file, { 7 + access: "public", 8 + }); 9 + }
+16
src/proxy.ts
··· 1 + import { headers } from "next/headers"; 2 + import { type NextRequest, NextResponse } from "next/server"; 3 + import { auth } from "@/lib/auth"; 4 + 5 + export async function proxy(request: NextRequest) { 6 + const session = await auth.api.getSession({ 7 + headers: await headers(), 8 + }); 9 + 10 + if (!session) return NextResponse.redirect(new URL("/auth", request.url)); 11 + return NextResponse.next(); 12 + } 13 + 14 + export const config = { 15 + matcher: ["/", "/settings", "/:id(\\d+)"], 16 + };