Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ To apply the same base UI in a project, run the following command:
npx shadcn@latest add "https://v0.dev/chat/b/b_fFQhsfElqQi"
```

#### 🏠 Homepage

The homepage layout was also crafted using v0. The generation prompt for it was:

```text
A minimal homepage for a Google Drive clone named T4S Drive. It should be just a
marketing page with a "get started" button. A gradient would be nice, please use
black and dark neutral grays.
```

### 🧰 Learn More about the T3 Stack

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

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

### 📝 Note from 7-4-2025

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

## 🎯 Fun Follow Ups

### Folder deletion

Make sure to fetch all of the folders that have it as a parent, and their
children too.
- [ ] **Folder creation**<br /> Make a server action that takes a name and
parentId, and creates a folder with that name and parentId (don't forget
to set the ownerId).
- [ ] **Folder deletion**<br /> Make sure to fetch all of the folders that have
it as a parent, and their children too.
- [ ] **Access Control**<br /> Check if user is owner before showing the folder
page.
- [ ] **Make a "file view" page**
- [ ] **Access control**
- [ ] **Toasts notifications**
35 changes: 35 additions & 0 deletions src/app/(home)/drive/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";

import { Button } from "~/components/ui/button";
import * as mutations from "~/server/db/mutations";
import * as queries from "~/server/db/queries";

export default async function DrivePage() {
const session = await auth();
if (!session.userId) return redirect("/sign-in");

const rootFolder = await queries.getRootFolderForUser(session.userId);

if (!rootFolder) {
return (
<form
action={async () => {
"use server";
const rootFolderId = await mutations.onboardUser(session.userId);
return redirect(`/f/${rootFolderId}`);
}}
>
<Button
size="lg"
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"
type="submit"
>
Create Your Drive
</Button>
</form>
);
}

return redirect(`/f/${rootFolder.id}`);
}
129 changes: 129 additions & 0 deletions src/app/(home)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Cloud, Shield, Users, Zap } from "lucide-react";
import Link from "next/link";

export default function HomePage({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-gradient-to-br from-black via-gray-900 to-gray-800 text-white">
{/* Navigation */}
<nav className="mx-auto flex max-w-7xl items-center justify-between p-6">
<div className="flex items-center space-x-2">
<Cloud className="h-8 w-8 text-white" />
<span className="text-2xl font-bold">T4S Drive</span>
</div>
<div className="hidden items-center space-x-8 md:flex">
<Link
href="#features"
className="text-gray-300 transition-colors hover:text-white"
>
Features
</Link>
<Link
href="#pricing"
className="text-gray-300 transition-colors hover:text-white"
>
Pricing
</Link>
<Link
href="#contact"
className="text-gray-300 transition-colors hover:text-white"
>
Contact
</Link>
</div>
</nav>

{/* Hero Section */}
<main className="mx-auto flex max-w-4xl flex-col items-center justify-center px-6 py-20 text-center">
<div className="space-y-8">
<h1 className="text-5xl font-bold tracking-tight md:text-7xl">
Your files,
<br />
<span className="bg-gradient-to-r from-gray-400 to-gray-200 bg-clip-text text-transparent">
everywhere
</span>
</h1>

<p className="mx-auto max-w-2xl text-xl leading-relaxed text-gray-300 md:text-2xl">
Store, sync, and share your files with T4S Drive. Access your
documents from any device, anywhere in the world.
</p>

{children}

<p className="pt-4 text-sm text-gray-400">
Free 15GB storage • No credit card required
</p>
</div>
</main>

{/* Features Section */}
<section className="mx-auto max-w-6xl px-6 py-20">
<div className="grid gap-8 md:grid-cols-3">
<div className="space-y-4 text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-gray-800">
<Shield className="h-8 w-8 text-gray-300" />
</div>
<h3 className="text-xl font-semibold">Secure Storage</h3>
<p className="text-gray-400">
Your files are encrypted and protected with enterprise-grade
security.
</p>
</div>

<div className="space-y-4 text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-gray-800">
<Zap className="h-8 w-8 text-gray-300" />
</div>
<h3 className="text-xl font-semibold">Lightning Fast</h3>
<p className="text-gray-400">
Upload and download files at blazing speeds with our global CDN.
</p>
</div>

<div className="space-y-4 text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-gray-800">
<Users className="h-8 w-8 text-gray-300" />
</div>
<h3 className="text-xl font-semibold">Easy Sharing</h3>
<p className="text-gray-400">
Share files and folders with anyone, with granular permission
controls.
</p>
</div>
</div>
</section>

{/* Footer */}
<footer className="mt-20 border-t border-gray-800 px-6 py-8">
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between md:flex-row">
<div className="mb-4 flex items-center space-x-2 md:mb-0">
<Cloud className="h-6 w-6 text-gray-400" />
<span className="text-gray-400">
© 2024 T4S Drive. All rights reserved.
</span>
</div>
<div className="flex space-x-6">
<Link
href="#"
className="text-gray-400 transition-colors hover:text-white"
>
Privacy
</Link>
<Link
href="#"
className="text-gray-400 transition-colors hover:text-white"
>
Terms
</Link>
<Link
href="#"
className="text-gray-400 transition-colors hover:text-white"
>
Support
</Link>
</div>
</div>
</footer>
</div>
);
}
26 changes: 24 additions & 2 deletions src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
export default async function HomePage() {
return <h1>Welcome to your Drive</h1>;
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";

import { Button } from "~/components/ui/button";

export default function HomePage() {
return (
<form
action={async () => {
"use server";
const session = await auth();
if (!session.userId) return redirect("/sign-in");
redirect("/drive");
}}
>
<Button
size="lg"
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"
type="submit"
>
Get Started
</Button>
</form>
);
}
15 changes: 15 additions & 0 deletions src/app/(home)/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SignInButton } from "@clerk/nextjs";

import { Button } from "~/components/ui/button";

export default function HomePage() {
return (
<Button
size="lg"
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"
asChild
>
<SignInButton forceRedirectUrl="/drive" />
</Button>
);
}
23 changes: 22 additions & 1 deletion src/server/db/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
import { db } from "~/server/db";
import { type File, files_table as filesSchema } from "~/server/db/schema";
import {
type File,
folders_table as folderSchema,
files_table as filesSchema,
} from "~/server/db/schema";

export async function onboardUser(userId: string) {
const [rootFolder] = await db
.insert(folderSchema)
.values({ name: "root", parent: null, ownerId: userId })
.$returningId();

const rootFolderId = rootFolder!.id;

await db.insert(folderSchema).values([
{ name: "Trash", parent: rootFolderId, ownerId: userId },
{ name: "Shared", parent: rootFolderId, ownerId: userId },
{ name: "Documents", parent: rootFolderId, ownerId: userId },
]);

return rootFolderId;
}

export function createFile(
file: Pick<File, "name" | "size" | "url" | "parent">,
Expand Down
10 changes: 9 additions & 1 deletion src/server/db/queries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "server-only"; // Ensure this file is only run on the server

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

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

export async function getRootFolderForUser(ownerId: string) {
const folders = await db
.select()
.from(folderSchema)
.where(and(eq(folderSchema.ownerId, ownerId), isNull(folderSchema.parent)));
return folders[0];
}

export function getAllFolders(folderId: number) {
return db
.select()
Expand Down