Skip to content

Commit 6a139b5

Browse files
authored
Onboarding 2025 (#68)
2 parents bb224f8 + 0c29f12 commit 6a139b5

File tree

12 files changed

+298
-90
lines changed

12 files changed

+298
-90
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/(main)/(search)/search/page.tsx

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,73 @@
1-
'use client'
1+
"use client"
22

3-
import { Button, MoreButton } from "@mav/ui/components/buttons";
4-
import { useState } from "react";
5-
import { LuArrowDownAZ, LuArrowUpZA, LuCat, LuGalleryThumbnails, LuListOrdered, LuScan, LuUser } from "react-icons/lu";
3+
import { Button, MoreButton } from "@mav/ui/components/buttons"
4+
import { useState } from "react"
5+
import {
6+
LuArrowDownAZ,
7+
LuArrowUpZA,
8+
LuCat,
9+
LuGalleryThumbnails,
10+
LuListOrdered,
11+
LuScan,
12+
LuUser
13+
} from "react-icons/lu"
614

715
export default function Search() {
816
const [sortingMode, setSortingMode] = useState("Best Matched")
9-
const [searchType, setSearchType] = useState("all");
17+
const [searchType, setSearchType] = useState("all")
1018
return (
1119
<div className="mx-auto max-w-screen-3xl py-6 px-8 flex flex-row gap-x-8">
1220
<div className="w-1/4 flex flex-col gap-y-3">
1321
<span className="text-2xl">Filters</span>
1422
<div className="gap-y-3 flex flex-col text-base">
15-
<Button onClick={() => setSearchType("all")} variant={searchType === "all" ? "primary" : "secondary"} icon={<LuScan size={18} />}>All</Button>
16-
<Button onClick={() => setSearchType("users")} variant={searchType === "users" ? "primary" : "secondary"} icon={<LuUser size={18} />}>Users & Artist</Button>
17-
<Button onClick={() => setSearchType("artworks")} variant={searchType === "artworks" ? "primary" : "secondary"} icon={<LuGalleryThumbnails size={18} />}>Artworks</Button>
18-
<Button onClick={() => setSearchType("listings")} variant={searchType === "listings" ? "primary" : "secondary"} icon={<LuListOrdered size={18} />}>Listing</Button>
19-
<Button onClick={() => setSearchType("adopts")} variant={searchType === "adopts" ? "primary" : "secondary"} icon={<LuCat size={18} />}>Adopts</Button>
23+
<Button
24+
onClick={() => setSearchType("all")}
25+
variant={searchType === "all" ? "primary" : "secondary"}
26+
icon={<LuScan size={18} />}
27+
>
28+
All
29+
</Button>
30+
<Button
31+
onClick={() => setSearchType("users")}
32+
variant={searchType === "users" ? "primary" : "secondary"}
33+
icon={<LuUser size={18} />}
34+
>
35+
Users & Artist
36+
</Button>
37+
<Button
38+
onClick={() => setSearchType("artworks")}
39+
variant={searchType === "artworks" ? "primary" : "secondary"}
40+
icon={<LuGalleryThumbnails size={18} />}
41+
>
42+
Artworks
43+
</Button>
44+
<Button
45+
onClick={() => setSearchType("listings")}
46+
variant={searchType === "listings" ? "primary" : "secondary"}
47+
icon={<LuListOrdered size={18} />}
48+
>
49+
Listing
50+
</Button>
51+
<Button
52+
onClick={() => setSearchType("adopts")}
53+
variant={searchType === "adopts" ? "primary" : "secondary"}
54+
icon={<LuCat size={18} />}
55+
>
56+
Adopts
57+
</Button>
2058
</div>
2159
</div>
2260
<div className="w-full">
2361
<div className="flex flex-row justify-between">
2462
<span className="text-2xl">621 results</span>
2563
<div className="flex flex-row gap-x-2">
26-
<Button variant="primary" icon={<LuArrowDownAZ size={18} />}>Sort by: {sortingMode}</Button>
64+
<Button variant="primary" icon={<LuArrowDownAZ size={18} />}>
65+
Sort by: {sortingMode}
66+
</Button>
2767
<MoreButton></MoreButton>
2868
</div>
2969
</div>
3070
</div>
3171
</div>
3272
)
3373
}
34-

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>

0 commit comments

Comments
 (0)