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
85 changes: 65 additions & 20 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { AnimatePresence } from 'framer-motion';
import { Navbar } from './components/Navbar';
import { Footer } from './components/Footer';
import { ErrorBoundary } from './components/ErrorBoundary';
import { AuthProvider } from './components/AuthProvider';
import { Home } from './pages/Home'; // Muat Home secara eager
import { Login } from './pages/Auth/Login';
import { Register } from './pages/Auth/Register';

// Lazy loaded pages
// Lazy loaded pages
const Karya = React.lazy(() => import('./pages/Karya').then(module => ({ default: module.Karya })));
const Tim = React.lazy(() => import('./pages/Tim').then(module => ({ default: module.Tim })));
Expand All @@ -18,8 +20,20 @@ const VideoPage = React.lazy(() => import('./pages/divisions/Video').then(module
const Writing = React.lazy(() => import('./pages/divisions/Writing').then(module => ({ default: module.Writing })));
const Meme = React.lazy(() => import('./pages/divisions/Meme').then(module => ({ default: module.Meme })));
const Coding = React.lazy(() => import('./pages/divisions/Coding').then(module => ({ default: module.Coding })));
const Studio = React.lazy(() => import('./pages/Studio').then(module => ({ default: module.Studio })));
const Settings = React.lazy(() => import('./pages/Settings').then(module => ({ default: module.Settings })));
const Profile = React.lazy(() => import('./pages/Profile').then(module => ({ default: module.Profile })));
const V5Launch = React.lazy(() => import('./pages/V5Launch').then(module => ({ default: module.V5Launch })));

// Admin Components
import { AdminGuard } from './components/AdminGuard';
import { AdminLayout } from './components/AdminLayout';
const AdminDashboard = React.lazy(() => import('./pages/Admin/Dashboard').then(module => ({ default: module.Dashboard })));
const AdminUsers = React.lazy(() => import('./pages/Admin/Users').then(module => ({ default: module.Users })));
const AdminContent = React.lazy(() => import('./pages/Admin/Content').then(module => ({ default: module.Content })));
const AdminAnnouncements = React.lazy(() => import('./pages/Admin/Announcements').then(module => ({ default: module.Announcements })));
const AdminSettings = React.lazy(() => import('./pages/Admin/Settings').then(module => ({ default: module.Settings })));

const Loading = () => (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="w-10 h-10 border-4 border-rose-500/30 border-t-rose-500 rounded-full animate-spin"></div>
Expand All @@ -44,6 +58,8 @@ const AnimatedRoutes = () => {
<Suspense fallback={<Loading />}>
<Routes location={location}>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/karya" element={<Karya />} />
<Route path="/tim" element={<Tim />} />
<Route path="/info" element={<Info />} />
Expand All @@ -54,7 +70,23 @@ const AnimatedRoutes = () => {
<Route path="/division/writing" element={<Writing />} />
<Route path="/division/meme" element={<Meme />} />
<Route path="/division/coding" element={<Coding />} />
<Route path="/studio" element={<Studio />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile/:username" element={<Profile />} />
<Route path="/v5-launch" element={<V5Launch />} />

{/* Admin Routes */}
<Route path="/admin" element={
<AdminGuard>
<AdminLayout />
</AdminGuard>
}>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<AdminUsers />} />
<Route path="content" element={<AdminContent />} />
<Route path="announcements" element={<AdminAnnouncements />} />
<Route path="settings" element={<AdminSettings />} />
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
Expand All @@ -64,6 +96,32 @@ const AnimatedRoutes = () => {
);
};

const AppContent = () => {
const { pathname } = useLocation();
const isStudio = pathname.toLowerCase() === '/studio';
const isAdmin = pathname.toLowerCase().startsWith('/admin');
const isZenMode = isStudio || isAdmin;

return (
<div className="min-h-screen bg-[#030303] text-white selection:bg-rose-500/30 font-sans overflow-x-hidden flex flex-col relative">
<div className="fixed inset-0 z-0 pointer-events-none overflow-hidden">
<div className="absolute inset-0 opacity-[0.02]" style={{ backgroundImage: `url("https://grainy-gradients.vercel.app/noise.svg")`, backgroundSize: '100px 100px' }}></div>
<div className="absolute top-[-20%] left-[10%] w-[800px] h-[800px] bg-rose-900/10 blur-[100px] rounded-full" />
<div className="absolute top-[20%] right-[-10%] w-[600px] h-[600px] bg-indigo-900/10 blur-[100px] rounded-full" />
<div className="absolute bottom-[-20%] left-[20%] w-[900px] h-[900px] bg-[#0a0a0a] blur-[80px] rounded-full" />
</div>

<div className="relative z-10 flex flex-col min-h-screen">
{!isZenMode && <Navbar />}
<main className={`flex-grow ${isStudio ? 'w-full h-screen overflow-hidden' : (isAdmin ? 'w-full px-0 container-none max-w-none' : 'container mx-auto px-4 md:px-6 lg:px-8 max-w-7xl')}`}>
<AnimatedRoutes />
</main>
{!isZenMode && <Footer />}
</div>
</div>
);
};

export default function App() {
const [mounted, setMounted] = useState(false);

Expand All @@ -75,25 +133,12 @@ export default function App() {

return (
<ErrorBoundary>
<Router>
<ScrollToTop />
<div className="min-h-screen bg-[#030303] text-white selection:bg-rose-500/30 font-sans overflow-x-hidden flex flex-col relative">
<div className="fixed inset-0 z-0 pointer-events-none overflow-hidden">
<div className="absolute inset-0 opacity-[0.02]" style={{ backgroundImage: `url("https://grainy-gradients.vercel.app/noise.svg")`, backgroundSize: '100px 100px' }}></div>
<div className="absolute top-[-20%] left-[10%] w-[800px] h-[800px] bg-rose-900/10 blur-[100px] rounded-full" />
<div className="absolute top-[20%] right-[-10%] w-[600px] h-[600px] bg-indigo-900/10 blur-[100px] rounded-full" />
<div className="absolute bottom-[-20%] left-[20%] w-[900px] h-[900px] bg-[#0a0a0a] blur-[80px] rounded-full" />
</div>

<div className="relative z-10 flex flex-col min-h-screen">
<Navbar />
<main className="flex-grow container mx-auto px-4 md:px-6 lg:px-8 max-w-7xl">
<AnimatedRoutes />
</main>
<Footer />
</div>
</div>
</Router>
<AuthProvider>
<Router>
<ScrollToTop />
<AppContent />
</Router>
</AuthProvider>
</ErrorBoundary>
);
}
25 changes: 25 additions & 0 deletions components/AdminGuard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from './AuthProvider';

interface AdminGuardProps {
children: React.ReactNode;
}

export const AdminGuard: React.FC<AdminGuardProps> = ({ children }) => {
const { profile, loading } = useAuth();

if (loading) {
return (
<div className="min-h-screen bg-[#030303] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-white/20 border-t-white rounded-full animate-spin" />
</div>
);
}

if (!profile || profile.role !== 'admin') {
return <Navigate to="/" replace />;
}

return <>{children}</>;
};
92 changes: 92 additions & 0 deletions components/AdminLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { Link, useLocation, Outlet } from 'react-router-dom';
import {
LayoutDashboard, Users, FileText, Megaphone,
Settings, LogOut, ArrowLeft
} from 'lucide-react';
import { useAuth } from './AuthProvider';
import { motion } from 'framer-motion';

export const AdminLayout = () => {
const location = useLocation();
const { signOut } = useAuth();

const menuItems = [
{ icon: LayoutDashboard, label: 'Beranda', path: '/admin' },
{ icon: Users, label: 'Pengguna', path: '/admin/users' },
{ icon: FileText, label: 'Konten', path: '/admin/content' },
{ icon: Megaphone, label: 'Pengumuman', path: '/admin/announcements' },
{ icon: Settings, label: 'Pengaturan', path: '/admin/settings' },
];

const isActive = (path: string) => {
if (path === '/admin') return location.pathname === '/admin';
return location.pathname.startsWith(path);
};

return (
<div className="flex min-h-screen bg-[#050505] text-white font-sans selection:bg-rose-500/30">
{/* Sidebar - Clean & Zen */}
<aside className="w-64 border-r border-white-[0.05] flex flex-col fixed inset-y-0 bg-[#080808] z-50">
<div className="p-8 pb-4">
<div className="flex items-center gap-3 mb-8">
<div className="w-9 h-9 bg-gradient-to-tr from-rose-500 to-rose-700 rounded-xl flex items-center justify-center font-bold text-lg shadow-[0_0_20px_rgba(225,29,72,0.3)]">
O
</div>
<div>
<h1 className="font-black tracking-tighter text-lg leading-none">OC.ADMIN</h1>
<span className="text-[9px] uppercase tracking-[0.2em] text-rose-500/80 font-bold">Zen System</span>
</div>
</div>
</div>

<nav className="flex-1 px-4 space-y-1 overflow-y-auto custom-scrollbar">
<div className="text-[10px] uppercase tracking-widest text-gray-500 font-bold px-4 mb-3">Menu Utama</div>
{menuItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 group ${isActive(item.path)
? 'bg-white/5 text-rose-500 font-bold'
: 'text-gray-400 hover:bg-white/[0.02] hover:text-white'
}`}
>
<item.icon size={18} className={`transition-transform duration-500 ${isActive(item.path) ? 'scale-110' : 'group-hover:scale-110'}`} />
<span className="text-sm tracking-tight">{item.label}</span>
{isActive(item.path) && (
<motion.div layoutId="activeNav" className="ml-auto w-1.5 h-1.5 rounded-full bg-rose-500 shadow-[0_0_10px_rgba(225,29,72,0.5)]" />
)}
</Link>
))}
</nav>

<div className="p-6 border-t border-white/[0.05] space-y-2">
<Link
to="/"
className="flex items-center gap-3 px-4 py-3 rounded-xl text-gray-500 hover:text-white transition-all text-sm group"
>
<ArrowLeft size={16} className="group-hover:-translate-x-1 transition-transform" />
Kembali ke Situs
</Link>
<button
onClick={signOut}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-gray-500 hover:text-red-400 transition-all text-sm group"
>
<LogOut size={16} className="group-hover:translate-x-1 transition-transform" />
Keluar Sesi
</button>
</div>
</aside>

{/* Main Content Area - Zen Mode */}
<main className="flex-1 ml-64 min-h-screen relative overflow-hidden">
{/* Minimal Background Glow */}
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-rose-500/5 blur-[120px] rounded-full pointer-events-none" />

<div className="relative z-10 p-10 max-w-6xl mx-auto">
<Outlet />
</div>
</main>
</div>
);
};
106 changes: 106 additions & 0 deletions components/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { User, Session } from '@supabase/supabase-js';
import { supabase } from '../lib/supabase';

interface Profile {
id: string;
username: string;
avatar_url: string;
is_approved: boolean;
role: string;
website?: string;
bio?: string;
}

interface AuthContextType {
session: Session | null;
user: User | null;
profile: Profile | null;
loading: boolean;
signOut: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [session, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
// 1. Get initial session
const getInitialSession = async () => {
try {
const { data: { session } } = await supabase.auth.getSession();
setSession(session);
setUser(session?.user ?? null);
if (session?.user) {
await fetchProfile(session.user.id);
}
} catch (err) {
console.error('Error getting session:', err);
} finally {
setLoading(false);
}
};

getInitialSession();

// 2. Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (_event, session) => {
setSession(session);
setUser(session?.user ?? null);

if (session?.user) {
await fetchProfile(session.user.id);
} else {
setProfile(null);
}
setLoading(false);
});

return () => {
subscription.unsubscribe();
};
}, []);

const fetchProfile = async (userId: string) => {
try {
const { data, error } = await supabase
.from('profiles')
.select('id, username, avatar_url, is_approved, role')
.eq('id', userId)
.single();

if (error) {
console.error('Error fetching profile:', error);
} else {
setProfile(data);
}
} catch (err) {
console.error('Error in fetchProfile:', err);
}
};

const signOut = async () => {
await supabase.auth.signOut();
setProfile(null);
setUser(null);
setSession(null);
};

return (
<AuthContext.Provider value={{ session, user, profile, loading, signOut }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
23 changes: 13 additions & 10 deletions components/BottomCTA.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { motion } from 'framer-motion';
import { ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';

export const BottomCTA = () => {
return (
Expand Down Expand Up @@ -32,16 +33,18 @@ export const BottomCTA = () => {
tanpa birokrasi, hanya kreativitas murni.
</motion.p>

<motion.button
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
className="px-10 py-4 bg-[#111] border border-white/20 hover:border-white/50 hover:bg-[#1a1a1a] text-white rounded-full font-bold text-sm transition-all flex items-center gap-3 mx-auto group shadow-lg"
>
Gabung Sekarang
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</motion.button>
<Link to="/register">
<motion.button
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
className="px-10 py-4 bg-[#111] border border-white/20 hover:border-white/50 hover:bg-[#1a1a1a] text-white rounded-full font-bold text-sm transition-all flex items-center gap-3 mx-auto group shadow-lg"
>
Gabung Sekarang
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</motion.button>
</Link>
</div>
</section>
);
Expand Down
Loading