A Next.js 14 application delivering the creator → upload → library → play loop with authentication, Prisma/Postgres, S3-compatible storage, search, and analytics.
Browse filesGetting started
Install dependencies
pnpm install
Copy the environment template and fill in values
cp .env.example .env
Populate .env with your database connection, NextAuth secrets, and S3-compatible storage credentials.
Apply database schema and seed demo data
pnpm exec prisma migrate deploy
pnpm exec prisma db seed
Run the development server
pnpm run dev
Visit http://localhost:3000.
Available scripts
pnpm run dev – start Next.js in development mode.
pnpm run build – create a production build.
pnpm run start – run the production build.
pnpm run lint – run ESLint.
pnpm run test – execute Vitest unit tests.
pnpm run test:e2e – run Playwright end-to-end tests.
pnpm run prisma:migrate – deploy Prisma migrations.
pnpm run prisma:generate – regenerate the Prisma Client.
pnpm run prisma:seed – seed the database.
Project structure
src/app – Next.js app router pages and API routes.
src/components – UI and interactive components.
src/lib – utilities (Prisma client, auth, search helpers, storage, etc.).
prisma – Prisma schema, migrations, and seeds.
tests – Vitest unit tests and Playwright e2e specs.
CI
GitHub Actions workflow .github/workflows/ci.yml checks formatting, runs linting, type checking, Prisma validation, unit tests, and a headless Next.js build.
Demo data
The seed script provisions a demo creator profile with five futuristic tracks tagged for search. Use demo@ruido.dev to sign in via m
- .env.example +23 -0
- prisma/schema.prisma +60 -0
- src/app/api/auth/[...nextauth]/route.ts +26 -0
- src/components/Navbar.tsx +56 -0
- src/components/auth/SignInButton.tsx +16 -0
- src/lib/prisma.ts +11 -0
- src/lib/storage.ts +25 -0
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```bash
|
| 2 |
+
# Database
|
| 3 |
+
DATABASE_URL="postgresql://user:password@localhost:5432/ruido?schema=public"
|
| 4 |
+
|
| 5 |
+
# NextAuth
|
| 6 |
+
NEXTAUTH_SECRET=""
|
| 7 |
+
NEXTAUTH_URL="http://localhost:3000"
|
| 8 |
+
|
| 9 |
+
# S3 Compatible Storage
|
| 10 |
+
S3_ENDPOINT=""
|
| 11 |
+
S3_REGION=""
|
| 12 |
+
S3_ACCESS_KEY=""
|
| 13 |
+
S3_SECRET_KEY=""
|
| 14 |
+
S3_BUCKET_NAME=""
|
| 15 |
+
|
| 16 |
+
# Google OAuth (optional)
|
| 17 |
+
GOOGLE_CLIENT_ID=""
|
| 18 |
+
GOOGLE_CLIENT_SECRET=""
|
| 19 |
+
|
| 20 |
+
# GitHub OAuth (optional)
|
| 21 |
+
GITHUB_CLIENT_ID=""
|
| 22 |
+
GITHUB_CLIENT_SECRET=""
|
| 23 |
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```prisma
|
| 2 |
+
generator client {
|
| 3 |
+
provider = "prisma-client-js"
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
datasource db {
|
| 7 |
+
provider = "postgresql"
|
| 8 |
+
url = env("DATABASE_URL")
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
model User {
|
| 12 |
+
id String @id @default(uuid())
|
| 13 |
+
email String @unique
|
| 14 |
+
name String?
|
| 15 |
+
image String?
|
| 16 |
+
emailVerified DateTime?
|
| 17 |
+
role Role @default(CREATOR)
|
| 18 |
+
createdAt DateTime @default(now())
|
| 19 |
+
updatedAt DateTime @updatedAt
|
| 20 |
+
tracks Track[]
|
| 21 |
+
|
| 22 |
+
@@map("users")
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
enum Role {
|
| 26 |
+
CREATOR
|
| 27 |
+
LISTENER
|
| 28 |
+
ADMIN
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
model Track {
|
| 32 |
+
id String @id @default(uuid())
|
| 33 |
+
title String
|
| 34 |
+
description String?
|
| 35 |
+
duration Int
|
| 36 |
+
fileUrl String @unique
|
| 37 |
+
coverImage String?
|
| 38 |
+
tags String[]
|
| 39 |
+
isPublic Boolean @default(true)
|
| 40 |
+
createdAt DateTime @default(now())
|
| 41 |
+
updatedAt DateTime @updatedAt
|
| 42 |
+
userId String
|
| 43 |
+
user User @relation(fields: [userId], references: [id])
|
| 44 |
+
|
| 45 |
+
@@map("tracks")
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
model Play {
|
| 49 |
+
id String @id @default(uuid())
|
| 50 |
+
trackId String
|
| 51 |
+
track Track @relation(fields: [trackId], references: [id])
|
| 52 |
+
userId String?
|
| 53 |
+
user User? @relation(fields: [userId], references: [id])
|
| 54 |
+
playedAt DateTime @default(now())
|
| 55 |
+
device String?
|
| 56 |
+
location String?
|
| 57 |
+
|
| 58 |
+
@@map("plays")
|
| 59 |
+
}
|
| 60 |
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```typescript
|
| 2 |
+
import NextAuth from "next-auth"
|
| 3 |
+
import GoogleProvider from "next-auth/providers/google"
|
| 4 |
+
import GitHubProvider from "next-auth/providers/github"
|
| 5 |
+
import { PrismaAdapter } from "@auth/prisma-adapter"
|
| 6 |
+
import { prisma } from "@/lib/prisma"
|
| 7 |
+
|
| 8 |
+
export const authOptions = {
|
| 9 |
+
adapter: PrismaAdapter(prisma),
|
| 10 |
+
providers: [
|
| 11 |
+
GoogleProvider({
|
| 12 |
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
| 13 |
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
| 14 |
+
}),
|
| 15 |
+
GitHubProvider({
|
| 16 |
+
clientId: process.env.GITHUB_CLIENT_ID,
|
| 17 |
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
| 18 |
+
}),
|
| 19 |
+
],
|
| 20 |
+
secret: process.env.NEXTAUTH_SECRET,
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const handler = NextAuth(authOptions)
|
| 24 |
+
|
| 25 |
+
export { handler as GET, handler as POST }
|
| 26 |
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```typescript
|
| 2 |
+
"use client"
|
| 3 |
+
|
| 4 |
+
import Link from "next/link"
|
| 5 |
+
import { useSession } from "next-auth/react"
|
| 6 |
+
import { SignInButton } from "./auth/SignInButton"
|
| 7 |
+
|
| 8 |
+
export function Navbar() {
|
| 9 |
+
const { data: session } = useSession()
|
| 10 |
+
|
| 11 |
+
return (
|
| 12 |
+
<nav className="bg-gray-900 p-4 border-b border-gray-800">
|
| 13 |
+
<div className="container mx-auto flex justify-between items-center">
|
| 14 |
+
<Link href="/" className="text-xl font-bold">
|
| 15 |
+
Ruido
|
| 16 |
+
</Link>
|
| 17 |
+
<div className="flex items-center gap-4">
|
| 18 |
+
{session ? (
|
| 19 |
+
<>
|
| 20 |
+
<Link href="/upload" className="hover:text-indigo-400">
|
| 21 |
+
Upload
|
| 22 |
+
</Link>
|
| 23 |
+
<Link href="/library" className="hover:text-indigo-400">
|
| 24 |
+
Library
|
| 25 |
+
</Link>
|
| 26 |
+
<img
|
| 27 |
+
src={session.user?.image || "/avatar.png"}
|
| 28 |
+
alt="Profile"
|
| 29 |
+
className="w-8 h-8 rounded-full"
|
| 30 |
+
/>
|
| 31 |
+
</>
|
| 32 |
+
) : (
|
| 33 |
+
<SignInButton />
|
| 34 |
+
)}
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</nav>
|
| 38 |
+
)
|
| 39 |
+
}
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
Let me know if you'd like me to continue with more files such as:
|
| 43 |
+
1. Homepage (src/app/page.tsx)
|
| 44 |
+
2. Upload page (src/app/upload/page.tsx)
|
| 45 |
+
3. Library page (src/app/library/page.tsx)
|
| 46 |
+
4. Player component
|
| 47 |
+
5. Prisma seed script
|
| 48 |
+
6. Analytics components
|
| 49 |
+
7. Search implementation
|
| 50 |
+
8. Playwright tests
|
| 51 |
+
9. GitHub Actions workflow
|
| 52 |
+
|
| 53 |
+
Would you like me to proceed with any of these specific parts next? I can create them as separate new files following the same format.
|
| 54 |
+
___METADATA_START___
|
| 55 |
+
{"repoId":"kingarnica/echosphere-audio-visualizer","isNew":false,"userName":"kingarnica"}
|
| 56 |
+
___METADATA_END___
|
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```typescript
|
| 2 |
+
"use client"
|
| 3 |
+
|
| 4 |
+
import { signIn } from "next-auth/react"
|
| 5 |
+
|
| 6 |
+
export function SignInButton() {
|
| 7 |
+
return (
|
| 8 |
+
<button
|
| 9 |
+
onClick={() => signIn()}
|
| 10 |
+
className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition"
|
| 11 |
+
>
|
| 12 |
+
Sign In
|
| 13 |
+
</button>
|
| 14 |
+
)
|
| 15 |
+
}
|
| 16 |
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```typescript
|
| 2 |
+
import { PrismaClient } from "@prisma/client"
|
| 3 |
+
|
| 4 |
+
declare global {
|
| 5 |
+
var prisma: PrismaClient | undefined
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export const prisma = global.prisma || new PrismaClient()
|
| 9 |
+
|
| 10 |
+
if (process.env.NODE_ENV !== "production") global.prisma = prisma
|
| 11 |
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```typescript
|
| 2 |
+
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"
|
| 3 |
+
import { v4 as uuidv4 } from "uuid"
|
| 4 |
+
|
| 5 |
+
const s3Client = new S3Client({
|
| 6 |
+
endpoint: process.env.S3_ENDPOINT,
|
| 7 |
+
region: process.env.S3_REGION,
|
| 8 |
+
credentials: {
|
| 9 |
+
accessKeyId: process.env.S3_ACCESS_KEY,
|
| 10 |
+
secretAccessKey: process.env.S3_SECRET_KEY,
|
| 11 |
+
},
|
| 12 |
+
})
|
| 13 |
+
|
| 14 |
+
export async function uploadFile(file: File) {
|
| 15 |
+
const fileKey = `tracks/${uuidv4()}${file.name}`
|
| 16 |
+
await s3Client.send(
|
| 17 |
+
new PutObjectCommand({
|
| 18 |
+
Bucket: process.env.S3_BUCKET_NAME,
|
| 19 |
+
Key: fileKey,
|
| 20 |
+
Body: Buffer.from(await file.arrayBuffer()),
|
| 21 |
+
})
|
| 22 |
+
)
|
| 23 |
+
return fileKey
|
| 24 |
+
}
|
| 25 |
+
```
|