Skip to content
This repository has been archived by the owner on May 22, 2024. It is now read-only.

Commit

Permalink
feat: better settings page (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrcaidev authored May 10, 2024
2 parents 1f62096 + 8cc0fa2 commit e82a626
Show file tree
Hide file tree
Showing 17 changed files with 409 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,6 @@ import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { createSupabaseServerClient } from "utils/supabase/server";

type UpdateNicknameState = {
nickname: string;
error: string;
};

export async function updateNickname(
state: UpdateNicknameState,
formData: FormData,
) {
const supabase = createSupabaseServerClient();

const nickname = formData.get("nickname")!.toString();

const { error } = await supabase.auth.updateUser({ data: { nickname } });

if (error) {
return { ...state, error: error.message };
}

return { nickname, error: "" };
}

export async function deleteUser() {
const supabase = createSupabaseServerClient();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { LoaderIcon, TrashIcon } from "lucide-react";
import { useActionState } from "react";
import { deleteUser } from "./actions";

export function DeleteUserButton() {
export function DeleteUserCard() {
const [{ error }, dispatch, isPending] = useActionState(deleteUser, {
error: "",
});
Expand All @@ -41,7 +41,10 @@ export function DeleteUserButton() {
continue with caution.
</CardDescription>
</CardHeader>
<CardFooter className="py-4 border-t border-destructive">
<CardFooter className="justify-between py-4 border-t border-destructive">
<p className="text-sm text-muted-foreground">
You will be asked to confirm this request.
</p>
<AlertDialog>
<AlertDialogTrigger
className={buttonVariants({ variant: "destructive" })}
Expand Down
27 changes: 27 additions & 0 deletions app/(with-framework)/settings/account/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { createSupabaseServerClient } from "utils/supabase/server";
import { DeleteUserCard } from "./delete-user-card";

export const metadata: Metadata = {
title: "Account",
description: "Change your account settings the iWallpaper platform",
};

export default async function SettingsAccountPage() {
const supabase = createSupabaseServerClient();

const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
redirect("/sign-in");
}

return (
<div className="space-y-4">
<DeleteUserCard />
</div>
);
}
15 changes: 15 additions & 0 deletions app/(with-framework)/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PageTitle } from "components/ui/page-title";
import type { PropsWithChildren } from "react";
import { NavigationBar } from "./navigation-bar";

export default function SettingsLayout({ children }: PropsWithChildren) {
return (
<>
<PageTitle>Settings</PageTitle>
<div className="flex flex-col md:flex-row gap-x-16 gap-y-8">
<NavigationBar />
<div className="grow">{children}</div>
</div>
</>
);
}
36 changes: 36 additions & 0 deletions app/(with-framework)/settings/navigation-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import { cn } from "components/ui/utils";
import Link from "next/link";
import { usePathname } from "next/navigation";

const links = [
{ text: "Profile", href: "/settings/profile" },
{ text: "Account", href: "/settings/account" },
{ text: "Third Party Services", href: "/settings/third-party-services" },
] as const;

export function NavigationBar() {
const pathname = usePathname();

return (
<aside className="shrink-0">
<nav className="flex flex-col gap-3 lg:min-w-64">
{links.map(({ href, text }) => (
<Link
key={href}
href={href}
className={cn(
"p-1 text-sm",
href === pathname
? "font-semibold"
: "text-muted-foreground hover:text-foreground transition-colors",
)}
>
{text}
</Link>
))}
</nav>
</aside>
);
}
44 changes: 0 additions & 44 deletions app/(with-framework)/settings/page.tsx

This file was deleted.

25 changes: 25 additions & 0 deletions app/(with-framework)/settings/profile/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use server";

import { createSupabaseServerClient } from "utils/supabase/server";

type UpdateNicknameState = {
nickname: string;
error: string;
};

export async function updateNickname(
state: UpdateNicknameState,
formData: FormData,
) {
const supabase = createSupabaseServerClient();

const nickname = formData.get("nickname")!.toString();

const { error } = await supabase.auth.updateUser({ data: { nickname } });

if (error) {
return { ...state, error: error.message };
}

return { nickname, error: "" };
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { Avatar, AvatarFallback, AvatarImage } from "components/ui/avatar";
import { buttonVariants } from "components/ui/button";
import { Button } from "components/ui/button";
import {
Card,
CardContent,
Expand All @@ -12,8 +12,7 @@ import {
} from "components/ui/card";
import { Input } from "components/ui/input";
import { useErrorToast } from "components/ui/use-toast";
import { cn } from "components/ui/utils";
import { LoaderIcon, UserCircleIcon } from "lucide-react";
import { LoaderIcon, UploadIcon, UserCircleIcon } from "lucide-react";
import { useActionState, type ChangeEvent } from "react";
import { createSupabaseBrowserClient } from "utils/supabase/browser";

Expand Down Expand Up @@ -83,30 +82,40 @@ export function AvatarCard({ initialAvatarUrl }: Props) {
useErrorToast(error);

return (
<Card className="shrink-0 px-6 py-2">
<Card className="relative">
<CardHeader>
<CardTitle>Your avatar</CardTitle>
<CardDescription>Make your profile stand out.</CardDescription>
<CardTitle>Avatar</CardTitle>
<CardDescription>
An avatar is optional but strongly recommended.
</CardDescription>
</CardHeader>
<CardContent>
<Avatar className="w-48 h-48 mx-auto">
<CardContent className="absolute right-0 top-5">
<Avatar className="w-16 h-16 mx-auto">
<AvatarImage src={avatarUrl} alt="Your avatar" />
<AvatarFallback>
<UserCircleIcon size={144} className="text-muted-foreground" />
<UserCircleIcon size={48} className="text-muted-foreground" />
</AvatarFallback>
</Avatar>
</CardContent>
<CardFooter>
<label
htmlFor="avatar"
className={cn(
buttonVariants({ variant: "outline" }),
"w-full cursor-pointer",
)}
>
{isPending && <LoaderIcon size={16} className="mr-2 animate-spin" />}
{isPending ? "Uploading avatar..." : "Change avatar"}
</label>
<CardFooter className="justify-between py-4 border-t">
<p className="text-sm text-muted-foreground">
An avatar image should not exceed 1MB.
</p>
<Button className="cursor-pointer" asChild>
<label htmlFor="avatar">
{isPending ? (
<>
<LoaderIcon size={16} className="mr-2 animate-spin" />
Uploading avatar...
</>
) : (
<>
<UploadIcon size={16} className="mr-2" />
Change avatar
</>
)}
</label>
</Button>
<Input
type="file"
accept="image/*"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export function NicknameCard({ initialNickname }: Props) {
<Card>
<CardHeader>
<CardTitle>Nickname</CardTitle>
<CardDescription>Give yourself a cool name!</CardDescription>
<CardDescription>
This is how your name will be displayed across the iWallpaper
platform.
</CardDescription>
</CardHeader>
<form action={dispatch}>
<CardContent>
Expand All @@ -45,7 +48,10 @@ export function NicknameCard({ initialNickname }: Props) {
id="nickname"
/>
</CardContent>
<CardFooter className="py-4 border-t">
<CardFooter className="justify-between py-4 border-t">
<p className="text-sm text-muted-foreground">
The length of a nickname should be between 2 and 20 characters.
</p>
<Button type="submit" disabled={isPending}>
{isPending ? (
<>
Expand Down
36 changes: 36 additions & 0 deletions app/(with-framework)/settings/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { createSupabaseServerClient } from "utils/supabase/server";
import { AvatarCard } from "./avatar-card";
import { NicknameCard } from "./nickname-card";

export const metadata: Metadata = {
title: "Profile",
description: "Change your profile on the iWallpaper platform",
};

export default async function SettingsProfilePage() {
const supabase = createSupabaseServerClient();

const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
redirect("/sign-in");
}

return (
<div className="space-y-4">
<AvatarCard initialAvatarUrl={user.user_metadata.avatar_url} />
<NicknameCard
initialNickname={
user.user_metadata.nickname ??
user.user_metadata.full_name ??
user.user_metadata.user_name ??
""
}
/>
</div>
);
}
46 changes: 46 additions & 0 deletions app/(with-framework)/settings/third-party-services/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use server";

import type { Provider, UserIdentity } from "@supabase/supabase-js";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { createSupabaseServerClient } from "utils/supabase/server";

type LinkIdentityState = {
identity: UserIdentity | undefined;
error: string;
};

export async function linkIdentity(
state: LinkIdentityState,
provider: Provider,
) {
const supabase = createSupabaseServerClient();

if (state.identity) {
const { error: unlinkError } = await supabase.auth.unlinkIdentity(
state.identity,
);

if (unlinkError) {
return { ...state, error: unlinkError.message };
}

return { identity: undefined, error: "" };
}

const { data: linkData, error: linkError } = await supabase.auth.linkIdentity(
{
provider,
options: {
redirectTo: `${process.env.ORIGIN}/auth/callback?next=/settings/third-party-services`,
},
},
);

if (linkError) {
return { ...state, error: linkError.message };
}

revalidatePath("/", "layout");
redirect(linkData.url);
}
Loading

0 comments on commit e82a626

Please sign in to comment.