This commit is contained in:
Jessica Rekcah 2025-11-29 10:25:34 +07:00
parent 22f85d731c
commit 0d339a35e2
26 changed files with 1650 additions and 122 deletions

View File

@ -8,9 +8,9 @@
### Phase 1: Project Setup & Foundation ### Phase 1: Project Setup & Foundation
- [ ] Initialize Next.js 16+ project with TypeScript - [x] Initialize Next.js 16+ project with TypeScript
- [ ] Install and configure Better Auth framework - [x] Install and configure Better Auth framework
- [ ] Set up Prisma ORM with database connection - [x] Set up Prisma ORM with database connection
- [ ] Configure project structure following AI_GUIDE.md patterns - [ ] Configure project structure following AI_GUIDE.md patterns
- [ ] Set up ESLint, TypeScript configuration - [ ] Set up ESLint, TypeScript configuration
- [ ] Create basic layout and routing structure - [ ] Create basic layout and routing structure

View File

@ -0,0 +1,170 @@
/**
* File: layout.tsx
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Dashboard layout for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
import Link from 'next/link';
import { ReactNode } from 'react';
export default function DashboardLayout({
children,
}: {
children: ReactNode;
}) {
return (
<div className="min-h-screen bg-gradient-to-br from-navy-900 via-navy-800 to-gray-900">
{/* Background overlay */}
<div className="absolute inset-0 bg-black/20"></div>
<div className="relative z-10 flex min-h-screen">
{/* Sidebar */}
<div className="w-64 bg-navy-900/50 backdrop-blur-sm border-r border-white/10">
<div className="p-6">
{/* Logo */}
<Link href="/dashboard" className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gold-400 rounded-lg flex items-center justify-center">
<span className="text-navy-900 font-bold text-sm">kV</span>
</div>
<span className="text-white font-bold text-xl">
kreati<span className="text-gold-400">Vortex</span>
</span>
</Link>
</div>
{/* Navigation */}
<nav className="px-4 pb-6">
<ul className="space-y-2">
<li>
<Link
href="/dashboard"
className="flex items-center space-x-3 px-4 py-3 text-gray-300 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 00-1-1v-4a1 1 0 011-1h2m4 0a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span>Beranda</span>
</Link>
</li>
<li>
<Link
href="/dashboard/teori"
className="flex items-center space-x-3 px-4 py-3 text-gray-300 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<span>Teori</span>
</Link>
</li>
<li>
<Link
href="/dashboard/praktik"
className="flex items-center space-x-3 px-4 py-3 text-gray-300 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span>Praktik</span>
</Link>
</li>
<li>
<Link
href="/dashboard/template-makalah"
className="flex items-center space-x-3 px-4 py-3 text-gray-300 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span>Template Makalah</span>
</Link>
</li>
</ul>
{/* Additional sections */}
<div className="mt-8">
<h3 className="px-4 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-4">
Komunitas
</h3>
<ul className="space-y-2">
<li>
<Link
href="/dashboard/videos"
className="flex items-center space-x-3 px-4 py-3 text-gray-300 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span>Video Saya</span>
</Link>
</li>
<li>
<Link
href="/dashboard/forum"
className="flex items-center space-x-3 px-4 py-3 text-gray-300 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z" />
</svg>
<span>Forum</span>
</Link>
</li>
<li>
<Link
href="/dashboard/assignments"
className="flex items-center space-x-3 px-4 py-3 text-gray-300 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2v2a2 2 0 01-2 2M9 5a2 2 0 012-2v2a2 2 0 01-2 2m-3 7h8m-8 4h8m-8 4h8" />
</svg>
<span>Tugas</span>
</Link>
</li>
</ul>
</div>
</nav>
</div>
{/* Main content */}
<div className="flex-1 overflow-auto">
{/* Top bar */}
<header className="bg-navy-800/50 backdrop-blur-sm border-b border-white/10 px-6 py-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-white">
Dashboard
</h1>
<div className="flex items-center space-x-4">
{/* Notifications */}
<button className="relative p-2 text-gray-300 hover:text-white transition-colors">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 10v6.159c0 .538.214 1.055.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span className="absolute top-1 right-1 w-2 h-2 bg-gold-400 rounded-full"></span>
</button>
{/* Profile */}
<div className="flex items-center space-x-3">
<div className="text-right">
<p className="text-sm font-medium text-white">John Doe</p>
<p className="text-xs text-gray-400">Calon Pendidik</p>
</div>
<div className="w-10 h-10 bg-gold-400 rounded-full flex items-center justify-center">
<span className="text-navy-900 font-semibold">JD</span>
</div>
</div>
</div>
</div>
</header>
{/* Page content */}
<main className="p-6">
{children}
</main>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,130 @@
/**
* File: page.tsx
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Dashboard page for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
export default function Dashboard() {
return (
<div>
<div className="mb-8">
<h2 className="text-2xl font-bold text-white mb-2">
Selamat Datang di kreatiVortex!
</h2>
<p className="text-gray-300">
Platform pembelajaran tari tradisional Indonesia
</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-300">Total Video</p>
<p className="text-2xl font-bold text-white">24</p>
</div>
<div className="w-12 h-12 bg-gold-400/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-gold-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
</div>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-300">Kelas Aktif</p>
<p className="text-2xl font-bold text-white">3</p>
</div>
<div className="w-12 h-12 bg-gold-400/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-gold-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
</div>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-300">Tugas Selesai</p>
<p className="text-2xl font-bold text-white">12</p>
</div>
<div className="w-12 h-12 bg-gold-400/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-gold-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm7-2a2 2 0 11-4 0v4a2 2 0 014 0V9z" />
</svg>
</div>
</div>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-300">Forum Post</p>
<p className="text-2xl font-bold text-white">48</p>
</div>
<div className="w-12 h-12 bg-gold-400/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-gold-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z" />
</svg>
</div>
</div>
</div>
</div>
{/* Recent Activity */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Recent Videos */}
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20">
<h3 className="text-lg font-semibold text-white mb-4">Video Terbaru</h3>
<div className="space-y-4">
<div className="flex items-center space-x-4">
<div className="w-16 h-12 bg-gold-400/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-gold-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
<div className="flex-1">
<h4 className="text-white font-medium">Tari Piring - Dasar</h4>
<p className="text-sm text-gray-400">2 jam yang lalu</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="w-16 h-12 bg-gold-400/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-gold-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
<div className="flex-1">
<h4 className="text-white font-medium">Tari Saman - Gerakan Tangan</h4>
<p className="text-sm text-gray-400">5 jam yang lalu</p>
</div>
</div>
</div>
</div>
{/* Recent Forum Posts */}
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20">
<h3 className="text-lg font-semibold text-white mb-4">Diskusi Terbaru</h3>
<div className="space-y-4">
<div className="border-l border-gold-400 pl-4">
<h4 className="text-white font-medium mb-1">Tips untuk pemula Tari Piring</h4>
<p className="text-sm text-gray-400 mb-2">Saya ingin berbagi beberapa tips untuk yang baru mulai belajar...</p>
<p className="text-xs text-gray-500">Oleh Sarah Pendidik 1 jam yang lalu</p>
</div>
<div className="border-l border-gold-400 pl-4">
<h4 className="text-white font-medium mb-1">Costum untuk pertunjukan</h4>
<p className="text-sm text-gray-400 mb-2">Apakah ada saran untuk costum yang tepat untuk pertunjukan tari...</p>
<p className="text-xs text-gray-500">Oleh Budi Student 3 jam yang lalu</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,17 @@
/**
* File: route.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Basic auth API for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ message: 'Auth API - GET' });
}
export async function POST() {
return NextResponse.json({ message: 'Auth API - POST' });
}

116
app/auth/signin/page.tsx Normal file
View File

@ -0,0 +1,116 @@
/**
* File: page.tsx
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Sign in page for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
export default function SignIn() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
// TODO: Implement actual sign in logic
setTimeout(() => {
setIsLoading(false);
router.push('/dashboard');
}, 1000);
};
return (
<div className="min-h-screen bg-gradient-to-br from-navy-900 via-navy-800 to-gray-900 flex items-center justify-center px-4">
<div className="absolute inset-0 bg-black/20"></div>
<div className="relative z-10 w-full max-w-md">
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 border border-white/20">
{/* Logo */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-white mb-2">
kreati<span className="text-gold-400">Vortex</span>
</h1>
<p className="text-gray-300">Masuk ke akun Anda</p>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gold-400 focus:border-transparent"
placeholder="nama@email.com"
required
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gold-400 focus:border-transparent"
placeholder="••••••••"
required
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember"
type="checkbox"
className="h-4 w-4 bg-white/10 border-white/20 rounded focus:ring-gold-400"
/>
<label htmlFor="remember" className="ml-2 block text-sm text-gray-300">
Ingat saya
</label>
</div>
<Link href="/auth/forgot-password" className="text-sm text-gold-400 hover:text-gold-300">
Lupa password?
</Link>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full py-3 px-4 bg-gold-500 hover:bg-gold-400 text-navy-900 font-semibold rounded-lg transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Memproses...' : 'Masuk'}
</button>
</form>
{/* Sign up link */}
<div className="mt-8 text-center">
<p className="text-gray-300">
Belum punya akun?{' '}
<Link href="/auth/signup" className="text-gold-400 hover:text-gold-300 font-medium">
Daftar sekarang
</Link>
</p>
</div>
</div>
</div>
</div>
);
}

168
app/auth/signup/page.tsx Normal file
View File

@ -0,0 +1,168 @@
/**
* File: page.tsx
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Sign up page for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
export default function SignUp() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
role: 'CALON_PENDIDIK' as 'CALON_PENDIDIK' | 'UMUM',
});
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (formData.password !== formData.confirmPassword) {
alert('Password tidak cocok!');
return;
}
setIsLoading(true);
// TODO: Implement actual sign up logic
setTimeout(() => {
setIsLoading(false);
router.push('/auth/signin');
}, 1000);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
return (
<div className="min-h-screen bg-gradient-to-br from-navy-900 via-navy-800 to-gray-900 flex items-center justify-center px-4 py-12">
<div className="absolute inset-0 bg-black/20"></div>
<div className="relative z-10 w-full max-w-md">
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 border border-white/20">
{/* Logo */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-white mb-2">
kreati<span className="text-gold-400">Vortex</span>
</h1>
<p className="text-gray-300">Buat akun baru</p>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-300 mb-2">
Nama Lengkap
</label>
<input
id="name"
name="name"
type="text"
value={formData.name}
onChange={handleChange}
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gold-400 focus:border-transparent"
placeholder="John Doe"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
Email
</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gold-400 focus:border-transparent"
placeholder="nama@email.com"
required
/>
</div>
<div>
<label htmlFor="role" className="block text-sm font-medium text-gray-300 mb-2">
Daftar sebagai
</label>
<select
id="role"
name="role"
value={formData.role}
onChange={handleChange}
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-gold-400 focus:border-transparent"
>
<option value="CALON_PENDIDIK" className="bg-navy-800">Calon Pendidik</option>
<option value="UMUM" className="bg-navy-800">Umum</option>
</select>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2">
Password
</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gold-400 focus:border-transparent"
placeholder="••••••••"
required
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-300 mb-2">
Konfirmasi Password
</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange}
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gold-400 focus:border-transparent"
placeholder="••••••••"
required
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full py-3 px-4 bg-gold-500 hover:bg-gold-400 text-navy-900 font-semibold rounded-lg transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Mendaftar...' : 'Daftar'}
</button>
</form>
{/* Sign in link */}
<div className="mt-8 text-center">
<p className="text-gray-300">
Sudah punya akun?{' '}
<Link href="/auth/signin" className="text-gold-400 hover:text-gold-300 font-medium">
Masuk
</Link>
</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -41,6 +41,28 @@
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px); --radius-xl: calc(var(--radius) + 4px);
/* kreatiVortex custom colors */
--color-navy-50: #f0f4ff;
--color-navy-100: #dae9ff;
--color-navy-200: #bdd7ff;
--color-navy-300: #8fc2ff;
--color-navy-400: #5ca7ff;
--color-navy-500: #3182ff;
--color-navy-600: #1e5fd6;
--color-navy-700: #1e40af;
--color-navy-800: #1e3a8a;
--color-navy-900: #000080;
--color-gold-50: #fffbeb;
--color-gold-100: #fef3c7;
--color-gold-200: #fde68a;
--color-gold-300: #fcd34d;
--color-gold-400: #fbbf24;
--color-gold-500: #f59e0b;
--color-gold-600: #d97706;
--color-gold-700: #b45309;
--color-gold-800: #92400e;
--color-gold-900: #78350f;
} }
:root { :root {

View File

@ -1,65 +1,96 @@
import Image from "next/image"; /**
* File: page.tsx
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Landing page for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
import Link from "next/link";
export default function Home() { export default function Home() {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <div className="min-h-screen bg-gradient-to-br from-navy-900 via-navy-800 to-gray-900 text-white">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> {/* Background overlay effect */}
<Image <div className="absolute inset-0 bg-black/20"></div>
className="dark:invert"
src="/next.svg" {/* Main content */}
alt="Next.js logo" <div className="relative z-10 flex min-h-screen flex-col items-center justify-center px-4">
width={100} <div className="max-w-4xl w-full text-center">
height={20} {/* Logo/Title */}
priority <div className="mb-12">
/> <h1 className="text-5xl md:text-7xl font-bold text-white mb-4">
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left"> kreati<span className="text-gold-400">Vortex</span>
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50"> </h1>
To get started, edit the page.tsx file. <p className="text-xl md:text-2xl text-gray-300">
</h1> Platform Pembelajaran Tari Online
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"> </p>
Looking for a starting point or more instructions? Head over to{" "} </div>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" {/* Description */}
className="font-medium text-zinc-950 dark:text-zinc-50" <div className="mb-16 max-w-2xl mx-auto">
<p className="text-lg text-gray-300 leading-relaxed">
Bergabunglah dengan komunitas pembelajaran tari tradisional Indonesia.
Pelajari berbagai tarian dari seluruh nusantara dengan panduan dari para ahli.
</p>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link
href="/auth/signin"
className="px-8 py-4 bg-navy-700 hover:bg-navy-600 text-white font-semibold rounded-lg transition-colors duration-200 min-w-[160px] text-center"
> >
Templates Masuk
</a>{" "} </Link>
or the{" "} <Link
<a href="/auth/signup"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" className="px-8 py-4 bg-gold-500 hover:bg-gold-400 text-navy-900 font-semibold rounded-lg transition-colors duration-200 min-w-[160px] text-center"
className="font-medium text-zinc-950 dark:text-zinc-50"
> >
Learning Daftar
</a>{" "} </Link>
center. <Link
</p> href="/auth/signup"
className="px-8 py-4 border-2 border-gold-400 text-gold-400 hover:bg-gold-400 hover:text-navy-900 font-semibold rounded-lg transition-colors duration-200 min-w-[160px] text-center"
>
Mulai Sekarang
</Link>
</div>
{/* Features */}
<div className="mt-24 grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="text-center">
<div className="w-16 h-16 bg-gold-400/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-gold-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</div>
<h3 className="text-xl font-semibold text-white mb-2">Video Pembelajaran</h3>
<p className="text-gray-400">Akses video tutorial tari dari berbagai daerah di Indonesia</p>
</div>
<div className="text-center">
<div className="w-16 h-16 bg-gold-400/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-gold-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-white mb-2">Forum Diskusi</h3>
<p className="text-gray-400">Berinteraksi dengan sesama pecinta tari dan para ahli</p>
</div>
<div className="text-center">
<div className="w-16 h-16 bg-gold-400/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-gold-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-white mb-2">Tugas & Evaluasi</h3>
<p className="text-gray-400">Kumpulkan tugas dan dapatkan feedback dari para pendidik</p>
</div>
</div>
</div> </div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row"> </div>
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div> </div>
); );
} }

View File

@ -6,7 +6,10 @@
"name": "kreativortex", "name": "kreativortex",
"dependencies": { "dependencies": {
"@prisma/adapter-pg": "^7.0.1", "@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"@types/bcryptjs": "^3.0.0",
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",
"bcryptjs": "^3.0.3",
"better-auth": "^1.4.3", "better-auth": "^1.4.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -272,6 +275,10 @@
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.0.1", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.0.1", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-01GpPPhLMoDMF4ipgfZz0L87fla/TV/PBQcmHy+9vV1ml6gUoqF8dUIRNI5Yf2YKpOwzQg9sn8C7dYD1Yio9Ug=="], "@prisma/adapter-pg": ["@prisma/adapter-pg@7.0.1", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.0.1", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-01GpPPhLMoDMF4ipgfZz0L87fla/TV/PBQcmHy+9vV1ml6gUoqF8dUIRNI5Yf2YKpOwzQg9sn8C7dYD1Yio9Ug=="],
"@prisma/client": ["@prisma/client@7.0.1", "", { "dependencies": { "@prisma/client-runtime-utils": "7.0.1" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-O74T6xcfaGAq5gXwCAvfTLvI6fmC3and2g5yLRMkNjri1K8mSpEgclDNuUWs9xj5AwNEMQ88NeD3asI+sovm1g=="],
"@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.0.1", "", {}, "sha512-R26BVX9D/iw4toUmZKZf3jniM/9pMGHHdZN5LVP2L7HNiCQKNQQx/9LuMtjepbgRqSqQO3oHN0yzojHLnKTGEw=="],
"@prisma/config": ["@prisma/config@7.0.1", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-MacIjXdo+hNKxPvtMzDXykIIc8HCRWoyjQ2nguJTFqLDzJBD5L6QRaANGTLOqbGtJ3sFvLRmfXhrFg3pWoK1BA=="], "@prisma/config": ["@prisma/config@7.0.1", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-MacIjXdo+hNKxPvtMzDXykIIc8HCRWoyjQ2nguJTFqLDzJBD5L6QRaANGTLOqbGtJ3sFvLRmfXhrFg3pWoK1BA=="],
"@prisma/debug": ["@prisma/debug@7.0.1", "", {}, "sha512-5+25XokVeAK2Z2C9W457AFw7Hk032Q3QI3G58KYKXPlpgxy+9FvV1+S1jqfJ2d4Nmq9LP/uACrM6OVhpJMSr8w=="], "@prisma/debug": ["@prisma/debug@7.0.1", "", {}, "sha512-5+25XokVeAK2Z2C9W457AFw7Hk032Q3QI3G58KYKXPlpgxy+9FvV1+S1jqfJ2d4Nmq9LP/uACrM6OVhpJMSr8w=="],
@ -330,6 +337,8 @@
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@ -446,6 +455,8 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.8.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw=="],
"bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
"better-auth": ["better-auth@1.4.3", "", { "dependencies": { "@better-auth/core": "1.4.3", "@better-auth/telemetry": "1.4.3", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@standard-schema/spec": "^1.0.0", "better-call": "1.1.0", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" } }, "sha512-cMY6PxXZ9Ep+KmLUcVEQ5RwtZtdawxTbDqUIgIIUYWJgq0KwNkQfFNimSYjHI0cNZwwAJyvbV42+uLogsDOUqQ=="], "better-auth": ["better-auth@1.4.3", "", { "dependencies": { "@better-auth/core": "1.4.3", "@better-auth/telemetry": "1.4.3", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@standard-schema/spec": "^1.0.0", "better-call": "1.1.0", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" } }, "sha512-cMY6PxXZ9Ep+KmLUcVEQ5RwtZtdawxTbDqUIgIIUYWJgq0KwNkQfFNimSYjHI0cNZwwAJyvbV42+uLogsDOUqQ=="],
"better-call": ["better-call@1.1.0", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1" } }, "sha512-7CecYG+yN8J1uBJni/Mpjryp8bW/YySYsrGEWgFe048ORASjq17keGjbKI2kHEOSc6u8pi11UxzkJ7jIovQw6w=="], "better-call": ["better-call@1.1.0", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1" } }, "sha512-7CecYG+yN8J1uBJni/Mpjryp8bW/YySYsrGEWgFe048ORASjq17keGjbKI2kHEOSc6u8pi11UxzkJ7jIovQw6w=="],

View File

@ -0,0 +1,9 @@
/**
* File: index.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: ActionButton component export
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
// ActionButton component will be created here

View File

@ -0,0 +1,9 @@
/**
* File: index.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Common components exports
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
// Common components will be exported here as they are created

View File

@ -0,0 +1,9 @@
/**
* File: index.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Forms components exports
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
// Form components will be exported here as they are created

View File

@ -0,0 +1,9 @@
/**
* File: index.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Layouts components exports
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
// Layout components will be exported here as they are created

9
components/index.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* File: index.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Main component exports for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
// Components will be exported here as they are created

13
lib/auth-client.ts Normal file
View File

@ -0,0 +1,13 @@
/**
* File: client.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Client-side auth configuration for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
});

View File

@ -1,3 +1,11 @@
/**
* File: auth.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Better Auth configuration for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma"; import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "./prisma"; import { prisma } from "./prisma";
@ -6,4 +14,21 @@ export const auth = betterAuth({
database: prismaAdapter(prisma, { database: prismaAdapter(prisma, {
provider: "postgresql", provider: "postgresql",
}), }),
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
},
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 1 day
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 minutes
},
},
account: {
accountLinking: {
enabled: false,
},
},
}); });

View File

@ -13,7 +13,10 @@
}, },
"dependencies": { "dependencies": {
"@prisma/adapter-pg": "^7.0.1", "@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"@types/bcryptjs": "^3.0.0",
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",
"bcryptjs": "^3.0.3",
"better-auth": "^1.4.3", "better-auth": "^1.4.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",

View File

@ -5,7 +5,7 @@ export default defineConfig({
schema: 'prisma', schema: 'prisma',
migrations: { migrations: {
path: 'prisma/migrations', path: 'prisma/migrations',
seed: 'tsx prisma/seed.ts', seed: 'node prisma/seed.js',
}, },
datasource: { datasource: {
url: env('DATABASE_URL'), url: env('DATABASE_URL'),

View File

@ -0,0 +1,78 @@
-- CreateTable
CREATE TABLE "user" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
"image" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "session" (
"id" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"token" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"ipAddress" TEXT,
"userAgent" TEXT,
"userId" TEXT NOT NULL,
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "account" (
"id" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"providerId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"accessToken" TEXT,
"refreshToken" TEXT,
"idToken" TEXT,
"accessTokenExpiresAt" TIMESTAMP(3),
"refreshTokenExpiresAt" TIMESTAMP(3),
"scope" TEXT,
"password" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verification" (
"id" TEXT NOT NULL,
"identifier" TEXT NOT NULL,
"value" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
-- CreateIndex
CREATE INDEX "session_userId_idx" ON "session"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
-- CreateIndex
CREATE INDEX "account_userId_idx" ON "account"("userId");
-- CreateIndex
CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
-- AddForeignKey
ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -1,62 +0,0 @@
model User {
id String @id
name String
email String
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
accounts Account[]
@@unique([email])
@@map("user")
}
model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@index([userId])
@@map("session")
}
model Account {
id String @id
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@map("account")
}
model Verification {
id String @id
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([identifier])
@@map("verification")
}

View File

@ -13,3 +13,311 @@ generator client {
datasource db { datasource db {
provider = "postgresql" provider = "postgresql"
} }
// ========================================
// AUTH MODELS (imported from auth.prisma)
// ========================================
// Note: User, Session, Account, and Verification models are imported from auth.prisma
// We need to extend the User model here to add the profile relation
model User {
id String @id
name String
email String
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
accounts Account[]
profile UserProfile?
@@unique([email])
@@map("user")
}
model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@index([userId])
@@map("session")
}
model Account {
id String @id
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@map("account")
}
model Verification {
id String @id
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([identifier])
@@map("verification")
}
// ========================================
// KREATIVORTEX PLATFORM MODELS
// ========================================
model UserRole {
id String @id @default(cuid())
name String @unique
description String
permissions String[] // JSON array of permissions
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users UserProfile[]
@@map("user_roles")
}
model UserProfile {
id String @id @default(cuid())
userId String @unique
roleId String
nim String? // Nomor Induk Mahasiswa
phone String?
address String?
bio String?
avatar String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role UserRole @relation(fields: [roleId], references: [id])
// Relations as educator
classesTeaching Class[]
videos Video[]
forums Forum[]
assignments Assignment[]
// Relations as student
classesEnrolled ClassMember[]
forumPosts ForumPost[] @relation("ForumPosts")
assignmentSubmissions AssignmentSubmission[]
comments Comment[] @relation("CommentAuthor")
// Additional relations
reviewedSubmissions AssignmentSubmission[] @relation("SubmissionReviewer")
@@map("user_profiles")
}
model Class {
id String @id @default(cuid())
name String
description String
code String @unique
educatorId String
isActive Boolean @default(true)
maxStudents Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String
updatedBy String
educator UserProfile @relation(fields: [educatorId], references: [id], onDelete: Cascade)
members ClassMember[]
videos Video[]
forums Forum[]
assignments Assignment[]
@@map("classes")
}
model ClassMember {
id String @id @default(cuid())
classId String
studentId String
joinedAt DateTime @default(now())
isActive Boolean @default(true)
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
student UserProfile @relation(fields: [studentId], references: [id], onDelete: Cascade)
@@unique([classId, studentId])
@@map("class_members")
}
model Video {
id String @id @default(cuid())
title String
description String?
videoUrl String
videoType VideoType @default(YOUTUBE)
thumbnailUrl String?
duration Int? // in seconds
uploaderId String
classId String?
isPublic Boolean @default(true)
viewCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String
updatedBy String
uploader UserProfile @relation(fields: [uploaderId], references: [id], onDelete: Cascade)
class Class? @relation(fields: [classId], references: [id], onDelete: SetNull)
comments Comment[]
@@map("videos")
}
model Forum {
id String @id @default(cuid())
title String
description String?
type ForumType @default(GENERAL)
classId String?
createdBy String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
updatedBy String
creator UserProfile @relation(fields: [createdBy], references: [id], onDelete: Cascade)
class Class? @relation(fields: [classId], references: [id], onDelete: SetNull)
posts ForumPost[]
@@map("forums")
}
model ForumPost {
id String @id @default(cuid())
title String
content String
forumId String
authorId String
parentId String? // for replies
isPinned Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
updatedBy String
forum Forum @relation(fields: [forumId], references: [id], onDelete: Cascade)
author UserProfile @relation("ForumPosts", fields: [authorId], references: [id], onDelete: Cascade)
parent ForumPost? @relation("PostReplies", fields: [parentId], references: [id], onDelete: Cascade)
replies ForumPost[] @relation("PostReplies")
comments Comment[]
@@map("forum_posts")
}
model Assignment {
id String @id @default(cuid())
title String
description String
dueDate DateTime
classId String
educatorId String
maxScore Int
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String
updatedBy String
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
educator UserProfile @relation(fields: [educatorId], references: [id], onDelete: Cascade)
submissions AssignmentSubmission[]
@@map("assignments")
}
model AssignmentSubmission {
id String @id @default(cuid())
assignmentId String
studentId String
documentUrl String
documentType String // MIME type
score Int?
feedback String?
status SubmissionStatus @default(SUBMITTED)
submittedAt DateTime @default(now())
reviewedAt DateTime?
reviewedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
updatedBy String
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
student UserProfile @relation(fields: [studentId], references: [id], onDelete: Cascade)
reviewer UserProfile? @relation("SubmissionReviewer", fields: [reviewedBy], references: [id], onDelete: SetNull)
@@map("assignment_submissions")
}
model Comment {
id String @id @default(cuid())
content String
authorId String
videoId String?
forumPostId String?
parentId String? // for replies
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
updatedBy String
author UserProfile @relation("CommentAuthor", fields: [authorId], references: [id], onDelete: Cascade)
video Video? @relation(fields: [videoId], references: [id], onDelete: Cascade)
forumPost ForumPost? @relation(fields: [forumPostId], references: [id], onDelete: Cascade)
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade)
replies Comment[] @relation("CommentReplies")
@@map("comments")
}
// ========================================
// ENUMS
// ========================================
enum VideoType {
YOUTUBE
LOCAL
}
enum ForumType {
GENERAL
CLASS
}
enum SubmissionStatus {
SUBMITTED
REVIEWED
REVISED
APPROVED
}

51
tailwind.config.ts Normal file
View File

@ -0,0 +1,51 @@
/**
* File: tailwind.config.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Tailwind CSS configuration for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: "class",
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
navy: {
50: '#f0f4ff',
100: '#dae9ff',
200: '#bdd7ff',
300: '#8fc2ff',
400: '#5ca7ff',
500: '#3182ff',
600: '#1e5fd6',
700: '#1e40af',
800: '#1e3a8a',
900: '#000080',
},
gold: {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
},
},
},
},
plugins: [],
};
export default config;

115
types/api.ts Normal file
View File

@ -0,0 +1,115 @@
/**
* File: api.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: API response type definitions for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
export interface ApiResponse<T = unknown> {
success: boolean;
message: string;
data?: T;
error?: string;
pagination?: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
export interface PaginationParams {
page?: number;
limit?: number;
search?: string;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
export interface VideoFilters extends PaginationParams {
uploaderId?: string;
classId?: string;
videoType?: 'YOUTUBE' | 'LOCAL';
isPublic?: boolean;
}
export interface ForumFilters extends PaginationParams {
type?: 'GENERAL' | 'CLASS';
classId?: string;
authorId?: string;
}
export interface AssignmentFilters extends PaginationParams {
classId?: string;
educatorId?: string;
status?: 'ACTIVE' | 'INACTIVE' | 'DUE_SOON' | 'OVERDUE';
}
export interface ClassFilters extends PaginationParams {
educatorId?: string;
isActive?: boolean;
}
export interface UserFilters extends PaginationParams {
role?: 'ADMINISTRATOR' | 'PENDIDIK' | 'CALON_PENDIDIK' | 'UMUM';
isActive?: boolean;
}
export interface CreateVideoData {
title: string;
description?: string;
videoUrl: string;
videoType: 'YOUTUBE' | 'LOCAL';
thumbnailUrl?: string;
classId?: string;
isPublic: boolean;
}
export interface CreateForumData {
title: string;
description?: string;
type: 'GENERAL' | 'CLASS';
classId?: string;
}
export interface CreateForumPostData {
title: string;
content: string;
forumId: string;
parentId?: string;
}
export interface CreateAssignmentData {
title: string;
description: string;
dueDate: Date;
classId: string;
maxScore: number;
}
export interface CreateClassData {
name: string;
description: string;
code: string;
maxStudents?: number;
}
export interface CreateCommentData {
content: string;
videoId?: string;
forumPostId?: string;
parentId?: string;
}
export interface UpdateUserData {
name?: string;
email?: string;
role?: 'ADMINISTRATOR' | 'PENDIDIK' | 'CALON_PENDIDIK' | 'UMUM';
profile?: {
phone?: string;
address?: string;
bio?: string;
avatar?: string;
};
}

110
types/auth.ts Normal file
View File

@ -0,0 +1,110 @@
/**
* File: auth.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Authentication type definitions for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
export interface AuthUser {
id: string;
name: string;
email: string;
role: 'ADMINISTRATOR' | 'PENDIDIK' | 'CALON_PENDIDIK' | 'UMUM';
image?: string;
profile?: {
nim?: string;
phone?: string;
bio?: string;
avatar?: string;
};
}
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterData {
name: string;
email: string;
password: string;
confirmPassword: string;
role?: 'CALON_PENDIDIK' | 'UMUM';
}
export interface RoleUpgradeData {
role: 'PENDIDIK' | 'CALON_PENDIDIK';
additionalInfo?: {
institution?: string;
experience?: string;
specialization?: string;
nim?: string;
};
}
export interface AuthResponse {
user: AuthUser;
token?: string;
message: string;
success: boolean;
}
export interface Permission {
canViewUsers: boolean;
canCreateUsers: boolean;
canUpdateUsers: boolean;
canDeleteUsers: boolean;
canManageClasses: boolean;
canManageVideos: boolean;
canManageForums: boolean;
canManageAssignments: boolean;
canViewReports: boolean;
}
export const ROLE_PERMISSIONS: Record<string, Permission> = {
ADMINISTRATOR: {
canViewUsers: true,
canCreateUsers: true,
canUpdateUsers: true,
canDeleteUsers: true,
canManageClasses: true,
canManageVideos: true,
canManageForums: true,
canManageAssignments: true,
canViewReports: true,
},
PENDIDIK: {
canViewUsers: false,
canCreateUsers: false,
canUpdateUsers: false,
canDeleteUsers: false,
canManageClasses: true,
canManageVideos: true,
canManageForums: true,
canManageAssignments: true,
canViewReports: false,
},
CALON_PENDIDIK: {
canViewUsers: false,
canCreateUsers: false,
canUpdateUsers: false,
canDeleteUsers: false,
canManageClasses: false,
canManageVideos: false,
canManageForums: true,
canManageAssignments: false,
canViewReports: false,
},
UMUM: {
canViewUsers: false,
canCreateUsers: false,
canUpdateUsers: false,
canDeleteUsers: false,
canManageClasses: false,
canManageVideos: false,
canManageForums: true,
canManageAssignments: false,
canViewReports: false,
},
};

174
types/prisma.ts Normal file
View File

@ -0,0 +1,174 @@
/**
* File: prisma.ts
* Created by: AI Assistant
* Date: 2025-11-29
* Purpose: Prisma type definitions for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
export interface UserWithRelations {
id: string;
name: string;
email: string;
emailVerified: boolean;
image?: string;
createdAt: Date;
updatedAt: Date;
role: UserRole;
profile?: UserProfile;
classesTeaching?: Class[];
classesEnrolled?: Class[];
videos?: Video[];
forumPosts?: ForumPost[];
assignments?: Assignment[];
}
export interface UserRole {
id: string;
name: 'ADMINISTRATOR' | 'PENDIDIK' | 'CALON_PENDIDIK' | 'UMUM';
description: string;
permissions: string[];
}
export interface UserProfile {
id: string;
userId: string;
nim?: string;
phone?: string;
address?: string;
bio?: string;
avatar?: string;
createdAt: Date;
updatedAt: Date;
}
export interface Class {
id: string;
name: string;
description: string;
code: string;
educatorId: string;
educator: UserWithRelations;
isActive: boolean;
maxStudents?: number;
createdAt: Date;
updatedAt: Date;
createdBy: string;
updatedBy: string;
students?: UserWithRelations[];
videos?: Video[];
forums?: Forum[];
assignments?: Assignment[];
}
export interface Video {
id: string;
title: string;
description?: string;
videoUrl: string;
videoType: 'YOUTUBE' | 'LOCAL';
thumbnailUrl?: string;
duration?: number;
uploaderId: string;
uploader: UserWithRelations;
classId?: string;
class?: Class;
isPublic: boolean;
viewCount: number;
createdAt: Date;
updatedAt: Date;
createdBy: string;
updatedBy: string;
comments?: Comment[];
}
export interface Forum {
id: string;
title: string;
description?: string;
type: 'GENERAL' | 'CLASS';
classId?: string;
class?: Class;
createdBy: string;
creator: UserWithRelations;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
updatedBy: string;
posts?: ForumPost[];
}
export interface ForumPost {
id: string;
title: string;
content: string;
forumId: string;
forum: Forum;
authorId: string;
author: UserWithRelations;
parentId?: string;
parent?: ForumPost;
replies?: ForumPost[];
isPinned: boolean;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
updatedBy: string;
comments?: Comment[];
}
export interface Assignment {
id: string;
title: string;
description: string;
dueDate: Date;
classId: string;
class: Class;
educatorId: string;
educator: UserWithRelations;
maxScore: number;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
createdBy: string;
updatedBy: string;
submissions?: AssignmentSubmission[];
}
export interface AssignmentSubmission {
id: string;
assignmentId: string;
assignment: Assignment;
studentId: string;
student: UserWithRelations;
documentUrl: string;
documentType: string;
score?: number;
feedback?: string;
status: 'SUBMITTED' | 'REVIEWED' | 'REVISED' | 'APPROVED';
submittedAt: Date;
reviewedAt?: Date;
reviewedBy?: string;
reviewer?: UserWithRelations;
createdAt: Date;
updatedAt: Date;
updatedBy: string;
}
export interface Comment {
id: string;
content: string;
authorId: string;
author: UserWithRelations;
videoId?: string;
video?: Video;
forumPostId?: string;
forumPost?: ForumPost;
parentId?: string;
parent?: Comment;
replies?: Comment[];
isActive: boolean;
createdAt: Date;
updatedAt: Date;
updatedBy: string;
}