crud user

This commit is contained in:
kizadl 2026-04-21 20:48:46 +07:00
parent 681059eccc
commit a786de2801
21 changed files with 718 additions and 73 deletions

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class DashboardController extends Controller
{
public function index()
{
$user = Auth::user();
return inertia('admin/Dashboard', [
'user' => $user,
]);
}
}

View File

@ -1,7 +1,8 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
use App\Models\User; use App\Models\User;
@ -17,7 +18,7 @@ public function index()
{ {
// //
$user = User::paginate(10); $user = User::paginate(10);
return Inertia::render('user/UserIndex', [ return Inertia::render('admin/user/UserIndex', [
'users' => $user, 'users' => $user,
]); ]);
} }
@ -28,7 +29,7 @@ public function index()
public function create() public function create()
{ {
// //
return Inertia::render('user/UserCreate'); return Inertia::render('admin/user/UserCreate');
} }
/** /**
@ -53,7 +54,7 @@ public function store(Request $request)
DB::commit(); DB::commit();
return redirect()->route('users.index')->with('success', 'User berhasil ditambahkan.'); return redirect()->route('admin.users.index')->with('success', 'User berhasil ditambahkan.');
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollback(); DB::rollback();
return redirect()->back()->with('error', 'Gagal menambahkan user: ' . $e->getMessage()); return redirect()->back()->with('error', 'Gagal menambahkan user: ' . $e->getMessage());
@ -75,7 +76,7 @@ public function edit(string $id)
{ {
// //
$user = User::findOrFail($id); $user = User::findOrFail($id);
return Inertia::render('user/UserEdit', [ return Inertia::render('admin/user/UserEdit', [
'user' => $user, 'user' => $user,
]); ]);
} }
@ -110,7 +111,7 @@ public function update(Request $request, string $id)
DB::commit(); DB::commit();
return redirect()->route('users.index')->with('success', 'User berhasil diupdate.'); return redirect()->route('admin.users.index')->with('success', 'User berhasil diupdate.');
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollback(); DB::rollback();
return redirect()->back()->with('error', 'Gagal mengupdate user: ' . $e->getMessage()); return redirect()->back()->with('error', 'Gagal mengupdate user: ' . $e->getMessage());
@ -126,7 +127,7 @@ public function destroy(string $id)
try { try {
$user = User::findOrFail($id); $user = User::findOrFail($id);
$user->delete(); $user->delete();
return redirect()->route('users.index')->with('success', 'User berhasil dihapus.'); return redirect()->route('admin.users.index')->with('success', 'User berhasil dihapus.');
} catch (\Exception $e) { } catch (\Exception $e) {
return redirect()->back()->with('error', 'Gagal menghapus user: ' . $e->getMessage()); return redirect()->back()->with('error', 'Gagal menghapus user: ' . $e->getMessage());
} }

View File

@ -56,7 +56,7 @@ const activeItemStyles =
const mainNavItems: NavItem[] = [ const mainNavItems: NavItem[] = [
{ {
title: 'Dashboard', title: 'Dashboard',
href: route('dashboard'), href: route('admin.dashboard'),
icon: LayoutGrid, icon: LayoutGrid,
}, },
]; ];
@ -146,7 +146,7 @@ const rightNavItems: NavItem[] = [
</Sheet> </Sheet>
</div> </div>
<Link :href="route('dashboard')" class="flex items-center gap-x-2"> <Link :href="route('admin.dashboard')" class="flex items-center gap-x-2">
<AppLogo /> <AppLogo />
</Link> </Link>

View File

@ -20,12 +20,12 @@ import type { NavItem } from '@/types';
const mainNavItems: NavItem[] = [ const mainNavItems: NavItem[] = [
{ {
title: 'Dashboard', title: 'Dashboard',
href: route('dashboard'), href: route('admin.dashboard'),
icon: LayoutGrid, icon: LayoutGrid,
}, },
{ {
title: 'Users', title: 'Users',
href: route('users.index'), href: route('admin.users.index'),
icon: Users, icon: Users,
}, },
]; ];
@ -50,7 +50,7 @@ const footerNavItems: NavItem[] = [
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton size="lg" as-child> <SidebarMenuButton size="lg" as-child>
<Link :href="route('dashboard')"> <Link :href="route('admin.dashboard')">
<AppLogo /> <AppLogo />
</Link> </Link>
</SidebarMenuButton> </SidebarMenuButton>

View File

@ -0,0 +1,150 @@
<template>
<section id="features" class="py-24 px-4">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4 font-display">Everything you need to ship</h2>
<p class="text-zinc-400 max-w-2xl mx-auto">
Built for modern teams. Powerful features that help you build, deploy, and scale faster than ever.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Large card - System Status -->
<div
class="md:col-span-2 group relative p-6 rounded-2xl bg-zinc-900 border border-zinc-800 hover:border-zinc-600 hover:scale-[1.02] transition-all duration-300 overflow-hidden"
>
<div class="flex items-start justify-between mb-8">
<div>
<div class="p-2 rounded-lg bg-zinc-800 w-fit mb-4">
<Activity class="w-5 h-5 text-zinc-400" :stroke-width="1.5" />
</div>
<h3 class="text-xl font-semibold text-white mb-2">Real-time Monitoring</h3>
<p class="text-zinc-400 text-sm">
Track system health, performance metrics, and alerts in real-time across all your deployments.
</p>
</div>
<div class="flex items-center gap-2">
<div
v-for="(active, i) in dots"
:key="i"
:class="[
'w-2 h-2 rounded-full',
active ? 'bg-emerald-500 animate-pulse' : 'bg-zinc-700',
]"
/>
</div>
</div>
<div class="grid grid-cols-4 gap-4">
<div v-for="metric in metrics" :key="metric" class="text-center">
<div class="text-2xl font-bold text-white mb-1">{{ Math.floor(Math.random() * 40 + 60) }}%</div>
<div class="text-xs text-zinc-500">{{ metric }}</div>
</div>
</div>
</div>
<!-- Command Palette -->
<div
class="group relative p-6 rounded-2xl bg-zinc-900 border border-zinc-800 hover:border-zinc-600 hover:scale-[1.02] transition-all duration-300"
>
<div class="p-2 rounded-lg bg-zinc-800 w-fit mb-4">
<Command class="w-5 h-5 text-zinc-400" :stroke-width="1.5" />
</div>
<h3 class="text-lg font-semibold text-white mb-2">Command Palette</h3>
<p class="text-zinc-400 text-sm mb-6">Navigate anywhere instantly with powerful keyboard shortcuts.</p>
<div class="flex items-center gap-1">
<kbd class="px-2 py-1 text-xs bg-zinc-800 border border-zinc-700 rounded text-zinc-300 font-mono">
</kbd>
<kbd class="px-2 py-1 text-xs bg-zinc-800 border border-zinc-700 rounded text-zinc-300 font-mono">
K
</kbd>
</div>
</div>
<!-- Analytics -->
<div
class="group relative p-6 rounded-2xl bg-zinc-900 border border-zinc-800 hover:border-zinc-600 hover:scale-[1.02] transition-all duration-300"
>
<div class="p-2 rounded-lg bg-zinc-800 w-fit mb-4">
<BarChart3 class="w-5 h-5 text-zinc-400" :stroke-width="1.5" />
</div>
<h3 class="text-lg font-semibold text-white mb-2">Analytics</h3>
<p class="text-zinc-400 text-sm mb-4">Deep insights into your application performance.</p>
<svg viewBox="0 0 100 70" class="w-full h-24">
<defs>
<linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="rgb(255,255,255)" stop-opacity="0.2" />
<stop offset="100%" stop-color="rgb(255,255,255)" stop-opacity="0" />
</linearGradient>
</defs>
<path
d="M 0 60 L 20 45 L 40 55 L 60 30 L 80 40 L 100 15 L 100 70 L 0 70 Z"
fill="url(#chartGradient)"
class="opacity-50"
/>
<path
d="M 0 60 L 20 45 L 40 55 L 60 30 L 80 40 L 100 15"
fill="none"
stroke="white"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</div>
<!-- Performance -->
<div
class="group relative p-6 rounded-2xl bg-zinc-900 border border-zinc-800 hover:border-zinc-600 hover:scale-[1.02] transition-all duration-300"
>
<div class="p-2 rounded-lg bg-zinc-800 w-fit mb-4">
<Zap class="w-5 h-5 text-zinc-400" :stroke-width="1.5" />
</div>
<h3 class="text-lg font-semibold text-white mb-2">Blazing Fast</h3>
<p class="text-zinc-400 text-sm mb-4">
Edge-optimized infrastructure for sub-50ms response times globally.
</p>
<div class="flex items-center gap-2 text-emerald-500 text-sm">
<span class="font-mono">~32ms</span>
<span class="text-zinc-500">avg response</span>
</div>
</div>
<!-- Security -->
<div
class="group relative p-6 rounded-2xl bg-zinc-900 border border-zinc-800 hover:border-zinc-600 hover:scale-[1.02] transition-all duration-300"
>
<div class="p-2 rounded-lg bg-zinc-800 w-fit mb-4">
<Shield class="w-5 h-5 text-zinc-400" :stroke-width="1.5" />
</div>
<h3 class="text-lg font-semibold text-white mb-2">Enterprise Security</h3>
<p class="text-zinc-400 text-sm mb-4">SOC2 compliant with end-to-end encryption and SSO support.</p>
<div class="flex items-center gap-2">
<span class="px-2 py-1 text-xs bg-zinc-800 rounded text-zinc-400">SOC2</span>
<span class="px-2 py-1 text-xs bg-zinc-800 rounded text-zinc-400">GDPR</span>
<span class="px-2 py-1 text-xs bg-zinc-800 rounded text-zinc-400">HIPAA</span>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { Activity, Command, BarChart3, Zap, Shield } from 'lucide-vue-next'
const dots = ref([true, true, true, false, true])
const metrics = ['CPU', 'Memory', 'Network', 'Storage']
let interval: NodeJS.Timeout
onMounted(() => {
interval = setInterval(() => {
dots.value = dots.value.map(() => Math.random() > 0.2)
}, 2000)
})
onBeforeUnmount(() => {
clearInterval(interval)
})
</script>

View File

@ -0,0 +1,32 @@
<template>
<section class="py-24 px-4">
<div class="max-w-4xl mx-auto text-center">
<h2 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-6 tracking-tight font-display">
Ready to ship faster?
</h2>
<p class="text-lg sm:text-xl text-zinc-400 mb-10 max-w-2xl mx-auto">
Join thousands of teams already building with Apex. Start free, no credit card required.
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
<button
class="shimmer-btn bg-white text-zinc-950 hover:bg-zinc-200 rounded-full px-8 py-3 text-base font-medium shadow-lg shadow-white/20 flex items-center gap-2"
>
Get Started for Free
<ArrowRight :size="20" />
</button>
<button
class="rounded-full px-8 py-3 text-base font-medium border border-zinc-800 text-zinc-300 hover:bg-zinc-900 hover:text-white hover:border-zinc-700 bg-transparent"
>
Talk to Sales
</button>
</div>
<p class="mt-8 text-sm text-zinc-500">Free forever for individuals. Team plans start at $29/month.</p>
</div>
</section>
</template>
<script setup lang="ts">
import { ArrowRight } from 'lucide-vue-next'
</script>

View File

@ -0,0 +1,55 @@
<template>
<footer class="border-t border-zinc-800 bg-zinc-950">
<div class="max-w-6xl mx-auto px-4 py-16">
<div class="grid grid-cols-2 md:grid-cols-5 gap-8">
<!-- Brand -->
<div class="col-span-2 md:col-span-1">
<a href="#" class="flex items-center gap-2 mb-4">
<div class="w-8 h-8 rounded-lg bg-white flex items-center justify-center">
<span class="text-zinc-950 font-bold text-sm">A</span>
</div>
<span class="font-semibold text-white">Apex</span>
</a>
<p class="text-sm text-zinc-500 mb-4">The modern platform for teams who ship fast.</p>
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-zinc-900 border border-zinc-800">
<span class="w-2 h-2 rounded-full bg-emerald-500 pulse-glow" />
<span class="text-xs text-zinc-400">All Systems Operational</span>
</div>
</div>
<!-- Links -->
<div v-for="(links, title) in footerLinks" :key="title">
<h4 class="text-sm font-semibold text-white mb-4">{{ title }}</h4>
<ul class="space-y-3">
<li v-for="link in links" :key="link">
<a href="#" class="text-sm text-zinc-500 hover:text-white transition-colors">
{{ link }}
</a>
</li>
</ul>
</div>
</div>
<!-- Bottom -->
<div
class="mt-16 pt-8 border-t border-zinc-800 flex flex-col sm:flex-row items-center justify-between gap-4"
>
<p class="text-sm text-zinc-500">&copy; {{ new Date().getFullYear() }} Apex, Inc. All rights reserved.</p>
<div class="flex items-center gap-6">
<a href="#" class="text-sm text-zinc-500 hover:text-white transition-colors">Twitter</a>
<a href="#" class="text-sm text-zinc-500 hover:text-white transition-colors">GitHub</a>
<a href="#" class="text-sm text-zinc-500 hover:text-white transition-colors">Discord</a>
</div>
</div>
</div>
</footer>
</template>
<script setup lang="ts">
const footerLinks: Record<string, string[]> = {
Product: ['Features', 'Pricing', 'Changelog', 'Roadmap', 'API'],
Resources: ['Documentation', 'Guides', 'Blog', 'Community', 'Templates'],
Company: ['About', 'Careers', 'Press', 'Partners', 'Contact'],
Legal: ['Privacy', 'Terms', 'Security', 'Cookies', 'Licenses'],
}
</script>

View File

@ -0,0 +1,78 @@
<template>
<section
class="relative min-h-screen flex flex-col items-center justify-center px-4 pt-24 pb-16 overflow-hidden"
>
<!-- Background gradient -->
<div
class="absolute inset-0 bg-gradient-to-b from-zinc-950 via-zinc-950 to-zinc-900 pointer-events-none"
/>
<!-- Subtle radial glow -->
<div
class="absolute top-1/4 left-1/2 -translate-x-1/2 w-[800px] h-[600px] bg-zinc-800/20 rounded-full blur-3xl pointer-events-none"
/>
<div class="relative z-10 max-w-5xl mx-auto text-center">
<!-- Badge -->
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-zinc-900 border border-zinc-800 mb-8">
<span class="w-2 h-2 rounded-full bg-emerald-500 pulse-glow" />
<span class="text-sm text-zinc-400">Now in Public Beta</span>
</div>
<!-- Headline -->
<h1 class="text-5xl sm:text-6xl lg:text-7xl font-bold tracking-tight text-white mb-6 font-display">
<span class="block">Ship faster.</span>
<span class="block text-zinc-500">Scale smarter.</span>
</h1>
<!-- Subheadline -->
<p class="text-lg sm:text-xl text-zinc-400 max-w-2xl mx-auto mb-10 leading-relaxed">
The modern platform for teams who ship fast. Built for scale, designed for speed. Everything you need to build,
deploy, and grow.
</p>
<!-- CTAs -->
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
<button
class="shimmer-btn bg-white text-zinc-950 hover:bg-zinc-200 rounded-full px-8 py-3 text-base font-medium shadow-lg shadow-white/10 flex items-center gap-2"
>
Start Building
<ArrowRight :size="16" />
</button>
<button
class="rounded-full px-8 py-3 text-base font-medium border border-zinc-800 text-zinc-300 hover:bg-zinc-900 hover:text-white hover:border-zinc-700 bg-transparent"
>
View Demo
</button>
</div>
<!-- Social Proof -->
<div class="flex flex-col items-center gap-4">
<div class="flex items-center -space-x-3">
<img
v-for="(avatar, index) in avatars"
:key="index"
:src="avatar"
:alt="`Avatar ${index + 1}`"
class="w-10 h-10 rounded-full border-2 border-zinc-950 object-cover"
/>
</div>
<p class="text-sm text-zinc-500">
Trusted by <span class="text-zinc-300 font-medium">2,000+</span> teams worldwide
</p>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ArrowRight } from 'lucide-vue-next'
const avatars = [
'/professional-headshot-1.png',
'/professional-headshot-2.png',
'/professional-headshot-3.png',
'/professional-headshot-4.png',
'/professional-headshot-5.png',
]
</script>

View File

@ -0,0 +1,42 @@
<template>
<section class="py-16 overflow-hidden">
<div class="text-center mb-10">
<p class="text-sm text-zinc-500 uppercase tracking-wider font-medium">Trusted by industry leaders</p>
</div>
<div class="relative">
<!-- Fade masks -->
<div
class="absolute left-0 top-0 bottom-0 w-32 bg-gradient-to-r from-zinc-950 to-transparent z-10 pointer-events-none"
/>
<div
class="absolute right-0 top-0 bottom-0 w-32 bg-gradient-to-l from-zinc-950 to-transparent z-10 pointer-events-none"
/>
<!-- Marquee container -->
<div class="flex animate-marquee">
<div v-for="logo in [...logos, ...logos]" :key="`${logo.name}-marquee`" class="flex items-center justify-center min-w-[160px] h-16 mx-8 grayscale opacity-50 hover:grayscale-0 hover:opacity-100 transition-all duration-300">
<div class="flex items-center gap-2 text-zinc-400">
<div class="w-8 h-8 rounded-lg bg-zinc-800 flex items-center justify-center">
<span class="text-xs font-bold">{{ logo.name[0] }}</span>
</div>
<span class="font-medium">{{ logo.name }}</span>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const logos = [
{ name: 'Vercel', width: 100 },
{ name: 'Stripe', width: 80 },
{ name: 'Linear', width: 90 },
{ name: 'Notion', width: 100 },
{ name: 'Figma', width: 70 },
{ name: 'Slack', width: 90 },
{ name: 'Discord', width: 100 },
{ name: 'GitHub', width: 90 },
]
</script>

View File

@ -0,0 +1,99 @@
<template>
<header class="fixed top-4 left-1/2 -translate-x-1/2 z-50 w-[calc(100%-2rem)] max-w-3xl">
<nav
class="relative flex items-center justify-between px-4 py-3 rounded-full bg-zinc-900/40 backdrop-blur-md border border-zinc-800"
>
<!-- Logo -->
<a href="#" class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-white flex items-center justify-center">
<span class="text-zinc-950 font-bold text-sm">A</span>
</div>
<span class="font-semibold text-white hidden sm:block">Apex</span>
</a>
<!-- Desktop Nav Items -->
<div class="hidden md:flex items-center gap-1 relative">
<a
v-for="(item, index) in navItems"
:key="item.label"
:href="item.href"
class="relative px-4 py-2 text-sm text-zinc-400 hover:text-white transition-colors"
@mouseenter="hoveredIndex = index"
@mouseleave="hoveredIndex = null"
>
<div
v-if="hoveredIndex === index"
class="absolute inset-0 bg-zinc-800 rounded-full"
/>
<span class="relative z-10">{{ item.label }}</span>
</a>
</div>
<!-- CTA Buttons -->
<div class="hidden md:flex items-center gap-3">
<button
class="px-4 py-2 text-sm text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors"
>
Sign In
</button>
<button
class="shimmer-btn bg-white text-zinc-950 hover:bg-zinc-200 rounded-full px-4 py-2 text-sm font-medium"
>
Get Started
</button>
</div>
<!-- Mobile Menu Button -->
<button
class="md:hidden p-2 text-zinc-400 hover:text-white"
@click="mobileMenuOpen = !mobileMenuOpen"
:aria-label="mobileMenuOpen ? 'Close menu' : 'Open menu'"
>
<Menu v-if="!mobileMenuOpen" :size="20" />
<X v-else :size="20" />
</button>
<!-- Mobile Menu -->
<div
v-if="mobileMenuOpen"
class="absolute top-full left-0 right-0 mt-2 p-4 rounded-2xl bg-zinc-900/95 backdrop-blur-md border border-zinc-800"
>
<div class="flex flex-col gap-2">
<a
v-for="item in navItems"
:key="item.label"
:href="item.href"
class="px-4 py-3 text-sm text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors"
@click="mobileMenuOpen = false"
>
{{ item.label }}
</a>
<hr class="border-zinc-800 my-2" />
<button class="px-4 py-3 text-sm text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors text-left">
Sign In
</button>
<button
class="shimmer-btn bg-white text-zinc-950 hover:bg-zinc-200 rounded-full px-4 py-2 text-sm font-medium w-full"
>
Get Started
</button>
</div>
</div>
</nav>
</header>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Menu, X } from 'lucide-vue-next'
const navItems = [
{ label: 'Features', href: '#features' },
{ label: 'Pricing', href: '#pricing' },
{ label: 'Docs', href: '#docs' },
{ label: 'Blog', href: '#blog' },
]
const hoveredIndex = ref<number | null>(null)
const mobileMenuOpen = ref(false)
</script>

View File

@ -0,0 +1,148 @@
<template>
<section id="pricing" class="py-24 px-4">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4 font-display">Simple, transparent pricing</h2>
<p class="text-zinc-400 max-w-2xl mx-auto mb-8">Start free, scale as you grow. No hidden fees, no surprises.</p>
<!-- Billing Toggle -->
<div class="inline-flex items-center p-1 rounded-full bg-zinc-900 border border-zinc-800">
<button
@click="billingCycle = 'monthly'"
:class="[
'relative px-4 py-2 text-sm font-medium rounded-full transition-colors',
billingCycle === 'monthly' ? 'text-white' : 'text-zinc-400',
]"
>
<div
v-if="billingCycle === 'monthly'"
class="absolute inset-0 bg-zinc-800 rounded-full"
/>
<span class="relative z-10">Monthly</span>
</button>
<button
@click="billingCycle = 'yearly'"
:class="[
'relative px-4 py-2 text-sm font-medium rounded-full transition-colors',
billingCycle === 'yearly' ? 'text-white' : 'text-zinc-400',
]"
>
<div
v-if="billingCycle === 'yearly'"
class="absolute inset-0 bg-zinc-800 rounded-full"
/>
<span class="relative z-10">Yearly</span>
<span class="relative z-10 ml-2 px-2 py-0.5 text-xs bg-emerald-500/20 text-emerald-400 rounded-full">
-20%
</span>
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div
v-for="(plan, index) in plans"
:key="plan.name"
:class="[
'relative p-6 rounded-2xl border transition-all duration-300 hover:scale-[1.02]',
plan.highlighted
? 'bg-zinc-900 border-zinc-700'
: 'bg-zinc-900/50 border-zinc-800 hover:border-zinc-600',
]"
>
<div
v-if="plan.highlighted"
class="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-1 bg-white text-zinc-950 text-xs font-medium rounded-full"
>
Most Popular
</div>
<div class="mb-6">
<h3 class="text-xl font-semibold text-white mb-2">{{ plan.name }}</h3>
<p class="text-zinc-400 text-sm">{{ plan.description }}</p>
</div>
<div class="mb-6">
<div class="flex items-baseline gap-1">
<span class="text-4xl font-bold text-white">${{ plan.price[billingCycle] }}</span>
<span v-if="plan.price.monthly > 0" class="text-zinc-400 text-sm">/month</span>
</div>
<p v-if="billingCycle === 'yearly' && plan.price.yearly > 0" class="text-xs text-zinc-500 mt-1">
Billed annually (${{ plan.price.yearly * 12 }}/year)
</p>
</div>
<ul class="space-y-3 mb-8">
<li v-for="feature in plan.features" :key="feature" class="flex items-center gap-3 text-sm text-zinc-300">
<Check class="w-4 h-4 text-emerald-500 shrink-0" :stroke-width="1.5" />
{{ feature }}
</li>
</ul>
<button
:class="[
'w-full rounded-full py-2 font-medium transition-colors',
plan.highlighted
? 'shimmer-btn bg-white text-zinc-950 hover:bg-zinc-200'
: 'bg-zinc-800 text-white hover:bg-zinc-700 border border-zinc-700',
]"
>
{{ plan.cta }}
</button>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Check } from 'lucide-vue-next'
type BillingCycle = 'monthly' | 'yearly'
const billingCycle = ref<BillingCycle>('monthly')
const plans = [
{
name: 'Starter',
description: 'Perfect for side projects and small teams',
price: { monthly: 0, yearly: 0 },
features: ['3 team members', '10 projects', 'Basic analytics', 'Community support', '1GB storage'],
cta: 'Get Started',
highlighted: false,
},
{
name: 'Pro',
description: 'For growing teams that need more power',
price: { monthly: 29, yearly: 24 },
features: [
'Unlimited team members',
'Unlimited projects',
'Advanced analytics',
'Priority support',
'100GB storage',
'Custom domains',
'API access',
],
cta: 'Start Free Trial',
highlighted: true,
},
{
name: 'Enterprise',
description: 'For organizations with advanced needs',
price: { monthly: 99, yearly: 79 },
features: [
'Everything in Pro',
'SSO & SAML',
'Dedicated support',
'SLA guarantee',
'Unlimited storage',
'Custom integrations',
'Audit logs',
],
cta: 'Contact Sales',
highlighted: false,
},
]
</script>

View File

@ -1,47 +0,0 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { route } from 'ziggy-js';
import PlaceholderPattern from '@/components/PlaceholderPattern.vue';
defineOptions({
layout: {
breadcrumbs: [
{
title: 'Dashboard',
href: route('dashboard'),
},
],
},
});
</script>
<template>
<Head title="Dashboard" />
<div
class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4"
>
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div
class="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
>
<PlaceholderPattern />
</div>
<div
class="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
>
<PlaceholderPattern />
</div>
<div
class="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
>
<PlaceholderPattern />
</div>
</div>
<div
class="relative min-h-[100vh] flex-1 rounded-xl border border-sidebar-border/70 md:min-h-min dark:border-sidebar-border"
>
<PlaceholderPattern />
</div>
</div>
</template>

View File

@ -26,7 +26,7 @@ withDefaults(
<nav class="flex items-center justify-end gap-4"> <nav class="flex items-center justify-end gap-4">
<Link <Link
v-if="$page.props.auth.user" v-if="$page.props.auth.user"
:href="route('dashboard')" :href="route('admin.dashboard')"
class="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]" class="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]"
> >
Dashboard Dashboard

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { route } from 'ziggy-js';
import PlaceholderPattern from '@/components/PlaceholderPattern.vue';
import { Card } from '@/components/ui/card';
defineOptions({
layout: {
breadcrumbs: [
{
title: 'Dashboard',
href: route('admin.dashboard'),
},
],
},
});
const { user } = defineProps<{ user: User }>();
</script>
<template>
<Head title="Dashboard" />
<div
class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4"
>
<!-- card greetings -->
<Card class="flex-1 border-border bg-card">
<div class="flex flex-col">
<div class="flex items-center justify-between">
<div>
<h3 class="text-base font-medium text-foreground">
Selamat Datang {{ user.name }}
</h3>
<p class="text-sm text-muted-foreground">
ini adalah dashboard admin
</p>
</div>
</div>
</div>
</Card>
</div>
</template>

View File

@ -107,7 +107,7 @@ const form = useForm({
}); });
const submit = () => { const submit = () => {
form.post(route('users.store'), { form.post(route('admin.users.store'), {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { onSuccess: () => {
form.reset(); form.reset();

View File

@ -41,7 +41,7 @@ const props = defineProps<{
const deleteUser = () => { const deleteUser = () => {
isDeleting.value = true; isDeleting.value = true;
router.delete(route('users.destroy', props.userId), { router.delete(route('admin.users.destroy', props.userId), {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { onSuccess: () => {
open.value = false; open.value = false;

View File

@ -126,7 +126,7 @@ watch(open, (value) => {
}); });
const submit = () => { const submit = () => {
form.put(route('users.update', props.user.id), { form.put(route('admin.users.update', props.user.id), {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { onSuccess: () => {
open.value = false; open.value = false;

View File

@ -11,7 +11,7 @@ defineOptions({
breadcrumbs: [ breadcrumbs: [
{ {
title: 'User', title: 'User',
href: route('users.index'), href: route('admin.users.index'),
}, },
], ],
}, },

View File

@ -0,0 +1,21 @@
<template>
<div class="bg-background text-foreground min-h-screen">
<Navbar />
<Hero />
<LogoMarquee />
<BentoGrid />
<Pricing />
<FinalCta />
<Footer />
</div>
</template>
<script setup lang="ts">
import Navbar from '@/components/public/Navbar.vue'
import Hero from '@/components/public/Hero.vue'
import LogoMarquee from '@/components/public/LogoMarquee.vue'
import BentoGrid from '@/components/public/BentoGrid.vue'
import Pricing from '@/components/public/Pricing.vue'
import FinalCta from '@/components/public/FinalCta.vue'
import Footer from '@/components/public/Footer.vue'
</script>

View File

@ -2,17 +2,22 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Laravel\Fortify\Features; use Laravel\Fortify\Features;
use App\Http\Controllers\UserController; use App\Http\Controllers\Admin\UserController;
use App\Http\Controllers\Admin\DashboardController;
Route::inertia('/', 'Welcome', [ Route::redirect('dashboard', 'admin/dashboard')->name('dashboard');
'canRegister' => Features::enabled(Features::registration()),
])->name('home');
Route::middleware(['auth', 'verified'])->group(function () { Route::get('/', function () {
Route::inertia('dashboard', 'Dashboard')->name('dashboard'); return inertia('public/Home', [
'canRegister' => Features::enabled(Features::registration()),
]);
})->name('home');
Route::prefix('admin')->name('admin.')->middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::resource('users', UserController::class)->names('users'); Route::resource('users', UserController::class)->names('users');
}); });
require __DIR__.'/settings.php'; require __DIR__ . '/settings.php';

View File

@ -3,7 +3,7 @@
use App\Models\User; use App\Models\User;
test('guests are redirected to the login page', function () { test('guests are redirected to the login page', function () {
$response = $this->get(route('dashboard')); $response = $this->get(route('admin.dashboard'));
$response->assertRedirect(route('login')); $response->assertRedirect(route('login'));
}); });
@ -11,6 +11,6 @@
$user = User::factory()->create(); $user = User::factory()->create();
$this->actingAs($user); $this->actingAs($user);
$response = $this->get(route('dashboard')); $response = $this->get(route('admin.dashboard'));
$response->assertOk(); $response->assertOk();
}); });