Skip to content

Commit 0c29f12

Browse files
committed
chore: onboarding
1 parent fa4b2c1 commit 0c29f12

File tree

8 files changed

+172
-51
lines changed

8 files changed

+172
-51
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use client'
2+
3+
import { useAuth } from "@/app/context/AuthContext";
4+
import AvatarSection from "./sections/Avatar";
5+
import { redirect } from "next/navigation";
6+
import { useState } from "react";
7+
import Creation from "./sections/Creation";
8+
9+
export default function OnboardingPage() {
10+
const { isLoading, user } = useAuth();
11+
const [section, setSection] = useState(0);
12+
const handleSuccess = () => setSection(prev => prev + 1);
13+
if (!isLoading && !user) return redirect('/login');
14+
15+
16+
if (!isLoading) {
17+
return (
18+
<div className="bg-100 relative flex min-h-screen w-full items-start justify-center px-6 pt-36">
19+
{section === 0 && <AvatarSection user={user} onSuccess={handleSuccess} />}
20+
{section === 1 && <Creation />}
21+
</div>
22+
);
23+
}
24+
25+
return null;
26+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use client'
2+
3+
import { DropZone } from "@/components/Modals/DropZone";
4+
import { useEffect, useState } from "react";
5+
import { updateProfile } from '@/utils/api'
6+
import { sleep } from "@/utils/utils";
7+
import Image from "next/image";
8+
import { User } from "@/app/context/AuthContext";
9+
10+
export default function AvatarSection({ user, onSuccess }: { user: User | null, onSuccess: () => void }) {
11+
const [uploadedUrl, setUploadedUrl] = useState("")
12+
13+
useEffect(() => {
14+
if (uploadedUrl) {
15+
updateProfile(uploadedUrl)
16+
sleep(3000).then(() => onSuccess());
17+
}
18+
}, [uploadedUrl])
19+
20+
return (
21+
<>
22+
<Image
23+
src="/Backdrop.png"
24+
alt="Backdrop"
25+
height={300}
26+
width={2000}
27+
className="absolute left-0 top-0 z-0 h-1/5 w-full object-cover"
28+
/>
29+
<div className="flex flex-col justify-center items-center gap-y-8 transition-all duration-300">
30+
<DropZone setData={setUploadedUrl} />
31+
{uploadedUrl ? (
32+
<h1 className="text-3xl opacity-100 transition-opacity duration-300">
33+
Looking good!
34+
</h1>
35+
) : (
36+
<h1 className="text-3xl opacity-100 transition-opacity duration-300">
37+
Welcome to MyArtverse, {user!.handle}!
38+
</h1>
39+
)}
40+
</div>
41+
</>
42+
43+
)
44+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react';
4+
import { fetchUserData } from '@/utils/api';
5+
import Image from 'next/image';
6+
7+
export default function Creation() {
8+
const [profile, setProfile] = useState<{ handle: string; avatarUrl: string } | null>(null);
9+
10+
useEffect(() => {
11+
const getProfile = async () => {
12+
const data = await fetchUserData();
13+
setProfile(data);
14+
};
15+
16+
getProfile();
17+
}, []);
18+
19+
if (!profile) return <div className="text-white">Loading...</div>;
20+
21+
return (
22+
<>
23+
<Image
24+
src="/Backdrop.png"
25+
alt="Backdrop"
26+
height={150}
27+
width={2000}
28+
className="absolute left-0 top-0 z-0 h-1/6 w-full object-cover"
29+
/>
30+
<div className='flex flex-col items-center'>
31+
<div className="flex items-center gap-x-4 bg-100 z-50 absolute p-4 rounded-lg">
32+
<Image alt={`@${profile.handle}'s Avatar`} src={profile.avatarUrl} width={50} height={50} />
33+
<span className='text-700 text-2xl'>@{profile.handle}</span>
34+
</div>
35+
<div className='relative flex flex-col items-center justify-center gap-y-8 pt-36'>
36+
<h1 className='text-3xl'>Create a Character</h1>
37+
<p className='text-2xl'>Awesome, you're avatar has been set.</p>
38+
<div className='flex flex-row gap-x-4'>
39+
<div className='bg-100 p-4 rounded-md inset-shadow-xs'>
40+
<Image src="/UserBanner.svg" alt="Banner" width={500} height={200} className="rounded-md mb-3" />
41+
<h1 className='text-2xl'>Create a Character</h1>
42+
<p className='text-xl'>Awesome, you're avatar has been set.</p>
43+
</div>
44+
<div className='bg-100 p-4 rounded-md inset-shadow-xs'>
45+
<Image src="/UserBanner.svg" alt="Banner" width={500} height={200} className="rounded-md mb-3" />
46+
<h1 className='text-2xl'>Import from Toyhou.se</h1>
47+
<p className='text-xl'>Awesome, you're avatar has been set.</p>
48+
</div>
49+
</div>
50+
</div>
51+
</div>
52+
</>
53+
);
54+
}

apps/web/src/app/context/AuthContext.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,26 @@ const AuthContext = createContext<AuthContextType>({
4646

4747
export const useAuth = () => useContext(AuthContext)
4848

49+
export const useAuthRedirect = (redirectTo: string = "/login") => {
50+
"use client"
51+
52+
const { user, isLoading } = useAuth()
53+
const router = useRouter()
54+
const [authUser, setAuthUser] = useState<User | null>(null)
55+
56+
useEffect(() => {
57+
if (!isLoading) {
58+
if (!user) {
59+
router.push(redirectTo)
60+
} else {
61+
setAuthUser(user)
62+
}
63+
}
64+
}, [user, isLoading, router, redirectTo])
65+
66+
return { user: authUser as User, isLoading }
67+
}
68+
4969
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
5070
const [user, setUser] = useState<User | null>(null)
5171
const [isLoading, setIsLoading] = useState(true)
@@ -79,6 +99,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
7999
throw new Error("Logout failed")
80100
}
81101
}
102+
82103

83104
return (
84105
<AuthContext.Provider value={{ user, isLoading, logout }}>

apps/web/src/app/context/authRedirect.tsx

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,7 @@ import { fetcher } from "@/app/lib/fetcher"
55
import { BACKEND_URL } from "@/utils/constants"
66
import { type User, useAuth } from "./AuthContext"
77

8-
export const useAuthRedirect = (redirectTo: string = "/login") => {
9-
"use client"
108

11-
const { user, isLoading } = useAuth()
12-
const router = useRouter()
13-
const [authUser, setAuthUser] = useState<User | null>(null)
14-
15-
useEffect(() => {
16-
if (!isLoading) {
17-
if (!user) {
18-
router.push(redirectTo)
19-
} else {
20-
setAuthUser(user)
21-
}
22-
}
23-
}, [user, isLoading, router, redirectTo])
24-
25-
return { user: authUser, isLoading }
26-
}
279

2810
export const serverAuthRedirect = async () => {
2911
"use server"

apps/web/src/components/Modals/DropZone.tsx

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,23 @@ import { LuUpload } from "react-icons/lu"
88
import type { MapElement } from "@/types/utils"
99

1010
const allowedTypes = ["image/png", "image/jpeg", "image/jpg"]
11-
const maxFileSize = 10 * 1024 * 1024 // 10 MB
11+
const maxFileSize = 10 * 1024 * 1024 // 10MB
1212

13-
export default function DropZone({
13+
export function DropZone({
1414
setData,
1515
className = "",
1616
value = "",
17-
aspectRatio = "1"
17+
onSuccess
1818
}: {
1919
setData: (url: string) => void
20+
onSuccess?: () => void
2021
className?: string
2122
value?: string
2223
aspectRatio?: string
2324
}) {
2425
const [isDragging, setIsDragging] = useState(false)
2526
const [file, setFile] = useState<File | null>(null)
26-
const [imageUrl, setImageUrl] = useState<string | null>(null)
27+
const [imageUrl, setImageUrl] = useState<string | null>(value || null)
2728
const [uploading, setUploading] = useState(false)
2829
const [error, setError] = useState<string | null>(null)
2930
const [success, setSuccess] = useState(false)
@@ -34,12 +35,6 @@ export default function DropZone({
3435
if (fileUploadRef.current) fileUploadRef.current.value = ""
3536
}, [file])
3637

37-
useEffect(() => {
38-
const handleDragOver = (e: DragEvent) => e.preventDefault()
39-
window.addEventListener("dragover", handleDragOver)
40-
return () => window.removeEventListener("dragover", handleDragOver)
41-
}, [])
42-
4338
const handleDrag = (e: React.DragEvent) => {
4439
e.preventDefault()
4540
e.stopPropagation()
@@ -59,7 +54,7 @@ export default function DropZone({
5954

6055
const processFile = (uploadedFile: File) => {
6156
if (!allowedTypes.includes(uploadedFile.type)) {
62-
return setError("Invalid file type.")
57+
return setError("Invalid file type. Please upload a PNG or JPEG image.")
6358
}
6459

6560
if (uploadedFile.size > maxFileSize) {
@@ -85,10 +80,7 @@ export default function DropZone({
8580
credentials: "include"
8681
})
8782

88-
if (!res.ok)
89-
throw new Error(
90-
res.status === 401 ? "Are you logged in?" : "Upload failed"
91-
)
83+
if (!res.ok) throw new Error("Upload failed. Please try again.")
9284

9385
const data = await res.json()
9486
setData(data.url)
@@ -98,47 +90,38 @@ export default function DropZone({
9890
setError(err instanceof Error ? err.message : "Unknown error occurred")
9991
} finally {
10092
setUploading(false)
93+
// onSuccess()
10194
}
10295
}
10396

10497
return (
10598
<div
10699
className={cn(
107-
"rounded-md border-2 border-dashed p-10 text-center transition-colors",
108-
isDragging ? "bg-gray-300" : "bg-gray-100",
100+
"flex flex-col items-center justify-center rounded-lg text-center cursor-pointer z-10 bg-100 w-fit p-8",
109101
className
110102
)}
111103
onDragEnter={handleDrag}
112104
onDragOver={handleDrag}
113105
onDragLeave={handleDrag}
114106
onDrop={handleDrop}
107+
onClick={() => fileUploadRef.current?.click()}
115108
>
116109
<input
117110
ref={fileUploadRef}
118111
type="file"
119112
className="hidden"
120113
onChange={handleFileInputChange}
114+
accept={allowedTypes.join(", ")}
121115
/>
122116
{imageUrl ? (
123-
<div className="flex flex-col items-center">
124-
<Image width={200} height={200} alt="Uploaded" src={imageUrl} />
125-
<span className="text-lg font-bold">Uploaded!</span>
126-
</div>
117+
<Image width={200} height={200} alt="Uploaded" src={imageUrl} className="rounded-md" />
127118
) : uploading ? (
128119
<span className="text-lg font-bold">Uploading...</span>
129120
) : (
130-
<div className="flex flex-col items-center">
131-
<button
132-
className="mb-6 flex items-center justify-center rounded-full bg-gray-200 p-8"
133-
onClick={() => fileUploadRef.current?.click()}
134-
>
135-
<LuUpload size={48} />
136-
</button>
137-
<span className="text-lg font-bold">Drag and drop files here</span>
138-
<span className="mt-4">
139-
Max size: 10MB, Supported formats: .jpg, .png
140-
</span>
141-
{error && <span className="text-red-500">{error}</span>}
121+
<div className="flex flex-col items-center justify-center text-sm cursor-pointer z-10 border-2 border-dashed aspect-square p-5 bg-100 rounded-lg">
122+
<span>Drag and drop files</span>
123+
<span>here or browse files</span>
124+
{error && <span className="text-red-500 mt-2">{error}</span>}
142125
</div>
143126
)}
144127
</div>

apps/web/src/utils/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,13 @@ export const search = async (
329329

330330
return data
331331
}
332+
333+
export const updateProfile = async (avatarLink?: string, birthday?: string, displayName?: string, pronouns?: string) => {
334+
const data = await apiWithAuth<unknown>('PUT', "/v1/profile/me", {
335+
avatarLink: avatarLink,
336+
birthday: birthday,
337+
displayName: birthday,
338+
pronouns: pronouns
339+
})
340+
return data;
341+
}

apps/web/src/utils/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

0 commit comments

Comments
 (0)