Skip to content

Commit 424c75a

Browse files
authored
Onboarding (#12)
1 parent 46dc54f commit 424c75a

File tree

7 files changed

+255
-9
lines changed

7 files changed

+255
-9
lines changed

README.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ To apply the same base UI in a project, run the following command:
3535
npx shadcn@latest add "https://v0.dev/chat/b/b_fFQhsfElqQi"
3636
```
3737

38+
#### 🏠 Homepage
39+
40+
The homepage layout was also crafted using v0. The generation prompt for it was:
41+
42+
```text
43+
A minimal homepage for a Google Drive clone named T4S Drive. It should be just a
44+
marketing page with a "get started" button. A gradient would be nice, please use
45+
black and dark neutral grays.
46+
```
47+
3848
### 🧰 Learn More about the T3 Stack
3949

4050
To explore more about the [T3 Stack](https://create.t3.gg/), refer to the
@@ -108,7 +118,7 @@ The database and UI are now connected, some improvements to make:
108118

109119
- [x] Change folders to link components, remove all client state
110120
- [x] Clean up the database and data fetching patterns
111-
- [ ] Real homepage
121+
- [x] Real homepage
112122

113123
### 📝 Note from 7-4-2025
114124

@@ -124,7 +134,13 @@ can be approved:
124134

125135
## 🎯 Fun Follow Ups
126136

127-
### Folder deletion
128-
129-
Make sure to fetch all of the folders that have it as a parent, and their
130-
children too.
137+
- [ ] **Folder creation**<br /> Make a server action that takes a name and
138+
parentId, and creates a folder with that name and parentId (don't forget
139+
to set the ownerId).
140+
- [ ] **Folder deletion**<br /> Make sure to fetch all of the folders that have
141+
it as a parent, and their children too.
142+
- [ ] **Access Control**<br /> Check if user is owner before showing the folder
143+
page.
144+
- [ ] **Make a "file view" page**
145+
- [ ] **Access control**
146+
- [ ] **Toasts notifications**

src/app/(home)/drive/page.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { auth } from "@clerk/nextjs/server";
2+
import { redirect } from "next/navigation";
3+
4+
import { Button } from "~/components/ui/button";
5+
import * as mutations from "~/server/db/mutations";
6+
import * as queries from "~/server/db/queries";
7+
8+
export default async function DrivePage() {
9+
const session = await auth();
10+
if (!session.userId) return redirect("/sign-in");
11+
12+
const rootFolder = await queries.getRootFolderForUser(session.userId);
13+
14+
if (!rootFolder) {
15+
return (
16+
<form
17+
action={async () => {
18+
"use server";
19+
const rootFolderId = await mutations.onboardUser(session.userId);
20+
return redirect(`/f/${rootFolderId}`);
21+
}}
22+
>
23+
<Button
24+
size="lg"
25+
className="cursor-pointer rounded-full bg-white px-8 py-6 text-lg font-semibold text-black transition-all duration-200 hover:scale-105 hover:bg-gray-100"
26+
type="submit"
27+
>
28+
Create Your Drive
29+
</Button>
30+
</form>
31+
);
32+
}
33+
34+
return redirect(`/f/${rootFolder.id}`);
35+
}

src/app/(home)/layout.tsx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { Cloud, Shield, Users, Zap } from "lucide-react";
2+
import Link from "next/link";
3+
4+
export default function HomePage({ children }: { children: React.ReactNode }) {
5+
return (
6+
<div className="min-h-screen bg-gradient-to-br from-black via-gray-900 to-gray-800 text-white">
7+
{/* Navigation */}
8+
<nav className="mx-auto flex max-w-7xl items-center justify-between p-6">
9+
<div className="flex items-center space-x-2">
10+
<Cloud className="h-8 w-8 text-white" />
11+
<span className="text-2xl font-bold">T4S Drive</span>
12+
</div>
13+
<div className="hidden items-center space-x-8 md:flex">
14+
<Link
15+
href="#features"
16+
className="text-gray-300 transition-colors hover:text-white"
17+
>
18+
Features
19+
</Link>
20+
<Link
21+
href="#pricing"
22+
className="text-gray-300 transition-colors hover:text-white"
23+
>
24+
Pricing
25+
</Link>
26+
<Link
27+
href="#contact"
28+
className="text-gray-300 transition-colors hover:text-white"
29+
>
30+
Contact
31+
</Link>
32+
</div>
33+
</nav>
34+
35+
{/* Hero Section */}
36+
<main className="mx-auto flex max-w-4xl flex-col items-center justify-center px-6 py-20 text-center">
37+
<div className="space-y-8">
38+
<h1 className="text-5xl font-bold tracking-tight md:text-7xl">
39+
Your files,
40+
<br />
41+
<span className="bg-gradient-to-r from-gray-400 to-gray-200 bg-clip-text text-transparent">
42+
everywhere
43+
</span>
44+
</h1>
45+
46+
<p className="mx-auto max-w-2xl text-xl leading-relaxed text-gray-300 md:text-2xl">
47+
Store, sync, and share your files with T4S Drive. Access your
48+
documents from any device, anywhere in the world.
49+
</p>
50+
51+
{children}
52+
53+
<p className="pt-4 text-sm text-gray-400">
54+
Free 15GB storage • No credit card required
55+
</p>
56+
</div>
57+
</main>
58+
59+
{/* Features Section */}
60+
<section className="mx-auto max-w-6xl px-6 py-20">
61+
<div className="grid gap-8 md:grid-cols-3">
62+
<div className="space-y-4 text-center">
63+
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-gray-800">
64+
<Shield className="h-8 w-8 text-gray-300" />
65+
</div>
66+
<h3 className="text-xl font-semibold">Secure Storage</h3>
67+
<p className="text-gray-400">
68+
Your files are encrypted and protected with enterprise-grade
69+
security.
70+
</p>
71+
</div>
72+
73+
<div className="space-y-4 text-center">
74+
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-gray-800">
75+
<Zap className="h-8 w-8 text-gray-300" />
76+
</div>
77+
<h3 className="text-xl font-semibold">Lightning Fast</h3>
78+
<p className="text-gray-400">
79+
Upload and download files at blazing speeds with our global CDN.
80+
</p>
81+
</div>
82+
83+
<div className="space-y-4 text-center">
84+
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-gray-800">
85+
<Users className="h-8 w-8 text-gray-300" />
86+
</div>
87+
<h3 className="text-xl font-semibold">Easy Sharing</h3>
88+
<p className="text-gray-400">
89+
Share files and folders with anyone, with granular permission
90+
controls.
91+
</p>
92+
</div>
93+
</div>
94+
</section>
95+
96+
{/* Footer */}
97+
<footer className="mt-20 border-t border-gray-800 px-6 py-8">
98+
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between md:flex-row">
99+
<div className="mb-4 flex items-center space-x-2 md:mb-0">
100+
<Cloud className="h-6 w-6 text-gray-400" />
101+
<span className="text-gray-400">
102+
© 2024 T4S Drive. All rights reserved.
103+
</span>
104+
</div>
105+
<div className="flex space-x-6">
106+
<Link
107+
href="#"
108+
className="text-gray-400 transition-colors hover:text-white"
109+
>
110+
Privacy
111+
</Link>
112+
<Link
113+
href="#"
114+
className="text-gray-400 transition-colors hover:text-white"
115+
>
116+
Terms
117+
</Link>
118+
<Link
119+
href="#"
120+
className="text-gray-400 transition-colors hover:text-white"
121+
>
122+
Support
123+
</Link>
124+
</div>
125+
</div>
126+
</footer>
127+
</div>
128+
);
129+
}

src/app/(home)/page.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1-
export default async function HomePage() {
2-
return <h1>Welcome to your Drive</h1>;
1+
import { auth } from "@clerk/nextjs/server";
2+
import { redirect } from "next/navigation";
3+
4+
import { Button } from "~/components/ui/button";
5+
6+
export default function HomePage() {
7+
return (
8+
<form
9+
action={async () => {
10+
"use server";
11+
const session = await auth();
12+
if (!session.userId) return redirect("/sign-in");
13+
redirect("/drive");
14+
}}
15+
>
16+
<Button
17+
size="lg"
18+
className="cursor-pointer rounded-full bg-white px-8 py-6 text-lg font-semibold text-black transition-all duration-200 hover:scale-105 hover:bg-gray-100"
19+
type="submit"
20+
>
21+
Get Started
22+
</Button>
23+
</form>
24+
);
325
}

src/app/(home)/sign-in/page.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { SignInButton } from "@clerk/nextjs";
2+
3+
import { Button } from "~/components/ui/button";
4+
5+
export default function HomePage() {
6+
return (
7+
<Button
8+
size="lg"
9+
className="cursor-pointer rounded-full bg-white px-8 py-6 text-lg font-semibold text-black transition-all duration-200 hover:scale-105 hover:bg-gray-100"
10+
asChild
11+
>
12+
<SignInButton forceRedirectUrl="/drive" />
13+
</Button>
14+
);
15+
}

src/server/db/mutations.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
import { db } from "~/server/db";
2-
import { type File, files_table as filesSchema } from "~/server/db/schema";
2+
import {
3+
type File,
4+
folders_table as folderSchema,
5+
files_table as filesSchema,
6+
} from "~/server/db/schema";
7+
8+
export async function onboardUser(userId: string) {
9+
const [rootFolder] = await db
10+
.insert(folderSchema)
11+
.values({ name: "root", parent: null, ownerId: userId })
12+
.$returningId();
13+
14+
const rootFolderId = rootFolder!.id;
15+
16+
await db.insert(folderSchema).values([
17+
{ name: "Trash", parent: rootFolderId, ownerId: userId },
18+
{ name: "Shared", parent: rootFolderId, ownerId: userId },
19+
{ name: "Documents", parent: rootFolderId, ownerId: userId },
20+
]);
21+
22+
return rootFolderId;
23+
}
324

425
export function createFile(
526
file: Pick<File, "name" | "size" | "url" | "parent">,

src/server/db/queries.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import "server-only"; // Ensure this file is only run on the server
22

3-
import { eq } from "drizzle-orm";
3+
import { and, eq, isNull } from "drizzle-orm";
44

55
import { db } from "~/server/db";
66
import {
@@ -27,6 +27,14 @@ export async function getAllParentsForFolder(folderId: number) {
2727
return parents;
2828
}
2929

30+
export async function getRootFolderForUser(ownerId: string) {
31+
const folders = await db
32+
.select()
33+
.from(folderSchema)
34+
.where(and(eq(folderSchema.ownerId, ownerId), isNull(folderSchema.parent)));
35+
return folders[0];
36+
}
37+
3038
export function getAllFolders(folderId: number) {
3139
return db
3240
.select()

0 commit comments

Comments
 (0)