Enhance onboarding experience with new page and data; update global styles and configuration. Added onboarding page with multi-step form, integrated business types and plans from JSON data, and improved global CSS with new color variables and scrollbar styles. Updated Next.js configuration for remote image patterns.

This commit is contained in:
Yoga Pangestu 2026-07-14 09:15:45 +07:00
parent d19d30efd2
commit 518103561e
5 changed files with 739 additions and 20 deletions

View File

@ -1,7 +1,14 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
};
export default nextConfig;

View File

@ -1,26 +1,163 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
:root {
--background: #ffffff;
--foreground: #171717;
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: #fafafa;
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius: var(--radius);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-poppins), var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--muted-foreground);
}
* {
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}

View File

@ -1,6 +1,9 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Geist, Geist_Mono, Poppins, Inter } from "next/font/google";
import "./globals.css";
import { cn } from "@/lib/utils";
const inter = Inter({ subsets: ['latin'], variable: '--font-sans' });
const geistSans = Geist({
variable: "--font-geist-sans",
@ -12,9 +15,15 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});
const poppins = Poppins({
variable: "--font-poppins",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Profitra - Kelola Bisnis Anda",
description: "Platform manajemen bisnis dengan POS, Inventory, Laporan Penjualan, dan lainnya.",
};
export default function RootLayout({
@ -24,8 +33,8 @@ export default function RootLayout({
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
lang="id"
className={cn("h-full", "antialiased", geistSans.variable, geistMono.variable, poppins.variable, "font-sans", inter.variable)}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>

439
src/app/onboarding/page.tsx Normal file
View File

@ -0,0 +1,439 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ArrowRightIcon, CheckCircle, ChevronRightIcon } from "lucide-react";
import onboardingData from "@/data/onboarding.json";
export default function OnboardingPage() {
const [step, setStep] = useState(1);
const [selectedBusiness, setSelectedBusiness] = useState<string | null>(null);
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
const [form, setForm] = useState({
businessName: "",
ownerName: "",
email: "",
whatsapp: "",
address: "",
currency: "IDR",
timezone: "Asia/Jakarta",
});
const handleNext = () => {
if (step < 3) setStep(step + 1);
};
const handleBack = () => {
if (step > 1) setStep(step - 1);
};
const handleFormChange = (name: string, value: string) => {
setForm({ ...form, [name]: value });
};
const handleSubmit = () => {
console.log("Onboarding complete:", { selectedBusiness, form, selectedPlan });
};
return (
<div className="min-h-screen lg:h-screen bg-background flex flex-col lg:flex-row lg:overflow-hidden">
{/* Left Panel */}
<div className="w-full lg:w-[60%] flex flex-col px-5 py-6 sm:px-6 sm:py-10 lg:px-20 lg:py-14 lg:h-full lg:overflow-hidden">
{/* Logo */}
<div className="mb-6 lg:mb-10 shrink-0">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
<span className="text-primary-foreground font-bold text-sm">P</span>
</div>
<span className="text-xl font-semibold tracking-tight text-foreground">
Profitra
</span>
</div>
</div>
{/* Step Indicator */}
<div className="mb-6 lg:mb-10 shrink-0">
<div className="flex items-center gap-1.5">
{[1, 2, 3].map((s) => (
<div key={s} className="flex items-center gap-1.5">
<div
className={`h-1.5 rounded-full transition-all ${step === s ? "w-6 bg-primary" : "w-2 bg-muted-foreground/30"
}`}
/>
{step === s && (
<span className="text-[11px] font-medium text-foreground whitespace-nowrap">
{onboardingData.stepLabels[String(s) as keyof typeof onboardingData.stepLabels]}
</span>
)}
</div>
))}
</div>
</div>
{/* Step Content */}
<div className="flex-1 lg:overflow-y-auto lg:min-h-0 pb-4 lg:pb-0 px-0.5">
{step === 1 && (
<StepOne
selected={selectedBusiness}
onSelect={setSelectedBusiness}
businessTypes={onboardingData.businessTypes}
/>
)}
{step === 2 && (
<StepTwo
form={form}
onChange={handleFormChange}
currencies={onboardingData.currencies}
timezones={onboardingData.timezones}
/>
)}
{step === 3 && (
<StepThree
selected={selectedPlan}
onSelect={setSelectedPlan}
plans={onboardingData.plans}
/>
)}
</div>
{/* Actions */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mt-6 lg:mt-10 pt-6 border-t border-border shrink-0">
<Button
variant="outline"
size="lg"
onClick={handleBack}
disabled={step <= 1}
className="order-2 sm:order-1"
>
Kembali
</Button>
{step < 3 ? (
<Button
size="lg"
onClick={handleNext}
disabled={step === 1 && !selectedBusiness}
className="order-1 sm:order-2"
>
Selanjutnya
<ChevronRightIcon className="ml-1 size-4" />
</Button>
) : (
<Button
size="lg"
onClick={handleSubmit}
className="order-1 sm:order-2"
>
Mulai Menggunakan Profitra
<ArrowRightIcon className="ml-1 size-4" />
</Button>
)}
</div>
{step === 1 && !selectedBusiness && (
<p className="text-sm text-muted-foreground mt-3 shrink-0">
Silakan pilih jenis bisnis terlebih dahulu.
</p>
)}
</div>
{/* Right Panel */}
<div className="hidden lg:flex w-[40%] relative border-l border-border overflow-hidden p-3">
<div className="relative w-full h-full rounded-xl overflow-hidden">
<Image
src={onboardingData.rightImages[String(step) as keyof typeof onboardingData.rightImages]}
alt="Onboarding illustration"
fill
className="object-cover transition-all duration-500"
sizes="40vw"
priority
/>
<div className="absolute inset-0 bg-linear-to-t from-black/70 via-black/20 to-transparent" />
<div className="absolute bottom-0 left-0 p-8 w-full">
<h2 className="text-xl font-semibold text-white mb-2 leading-tight text-left">
{onboardingData.rightPanelContent[String(step) as keyof typeof onboardingData.rightPanelContent].title}
</h2>
<p className="text-white/70 text-sm leading-relaxed max-w-xs text-left">
{onboardingData.rightPanelContent[String(step) as keyof typeof onboardingData.rightPanelContent].description}
</p>
</div>
</div>
</div>
</div>
);
}
function StepOne({
selected,
onSelect,
businessTypes,
}: {
selected: string | null;
onSelect: (val: string) => void;
businessTypes: { label: string; image: string }[];
}) {
return (
<div>
<h2 className="text-2xl font-semibold tracking-tight text-foreground mb-2">
Pilih Jenis Bisnis Anda
</h2>
<p className="text-muted-foreground text-sm mb-8 leading-relaxed">
Profitra akan menyesuaikan fitur dan pengaturan awal berdasarkan jenis
usaha yang Anda jalankan.
</p>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2.5 pb-2 px-0.5 py-1">
{businessTypes.map((type) => (
<button
key={type.label}
onClick={() => onSelect(type.label)}
className={`group relative aspect-4/3 rounded-lg overflow-hidden text-left transition-all duration-150 ${selected === type.label
? "ring-2 ring-primary ring-offset-2 shadow-md"
: "ring-1 ring-border hover:ring-foreground/30 hover:shadow-sm"
}`}
>
<Image
src={type.image}
alt={type.label}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
/>
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-black/20 to-transparent" />
<div className="relative h-full flex flex-col justify-end p-3">
<span className="text-sm font-medium text-white drop-shadow-sm">
{type.label}
</span>
</div>
{selected === type.label && (
<div className="absolute top-2 right-2 w-5 h-5 rounded-full bg-primary flex items-center justify-center">
<CheckCircle className="size-3 text-primary-foreground" strokeWidth={3} />
</div>
)}
</button>
))}
</div>
</div>
);
}
function StepTwo({
form,
onChange,
currencies,
timezones,
}: {
form: {
businessName: string;
ownerName: string;
email: string;
whatsapp: string;
address: string;
currency: string;
timezone: string;
};
onChange: (name: string, value: string) => void;
currencies: { value: string; label: string }[];
timezones: { value: string; label: string }[];
}) {
return (
<div>
<h2 className="text-2xl font-semibold tracking-tight text-foreground mb-2">
Lengkapi Informasi Bisnis
</h2>
<p className="text-muted-foreground text-sm mb-8 leading-relaxed">
Masukkan informasi dasar mengenai usaha Anda.
</p>
<div className="space-y-5">
<div className="space-y-2">
<Label>Nama Usaha</Label>
<Input
value={form.businessName}
onChange={(e) => onChange("businessName", e.target.value)}
placeholder="Contoh: Warung Berkah"
/>
</div>
<div className="space-y-2">
<Label>Nama Pemilik</Label>
<Input
value={form.ownerName}
onChange={(e) => onChange("ownerName", e.target.value)}
placeholder="Yoga Pangestu"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Email Bisnis</Label>
<Input
type="email"
value={form.email}
onChange={(e) => onChange("email", e.target.value)}
placeholder="owner@email.com"
/>
</div>
<div className="space-y-2">
<Label>Nomor WhatsApp</Label>
<Input
type="tel"
value={form.whatsapp}
onChange={(e) => onChange("whatsapp", e.target.value)}
placeholder="0812xxxxxxxx"
/>
</div>
</div>
<div className="space-y-2">
<Label>Alamat</Label>
<Textarea
value={form.address}
onChange={(e) => onChange("address", e.target.value)}
rows={3}
placeholder="Alamat lengkap usaha Anda"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Mata Uang</Label>
<Select value={form.currency} onValueChange={(val) => val && onChange("currency", val)}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{currencies.map((currency) => (
<SelectItem key={currency.value} value={currency.value}>{currency.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Zona Waktu</Label>
<Select value={form.timezone} onValueChange={(val) => val && onChange("timezone", val)}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{timezones.map((tz) => (
<SelectItem key={tz.value} value={tz.value}>{tz.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
);
}
function StepThree({
selected,
onSelect,
plans,
}: {
selected: string | null;
onSelect: (val: string) => void;
plans: {
name: string;
price: string;
priceSub: string;
description: string;
features: string[];
buttonLabel: string;
popular: boolean;
}[];
}) {
return (
<div>
<h2 className="text-2xl font-semibold tracking-tight text-foreground mb-2">
Pilih Paket Profitra
</h2>
<p className="text-muted-foreground text-sm mb-8 leading-relaxed">
Mulai gratis dan upgrade kapan saja sesuai perkembangan bisnis Anda.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 px-0.5 py-1">
{plans.map((plan) => (
<Card
key={plan.name}
onClick={() => onSelect(plan.name)}
className={`relative cursor-pointer transition-all duration-150 ${selected === plan.name
? "border-primary bg-primary/5 shadow-sm ring-1 ring-primary"
: "hover:border-foreground/20 hover:bg-accent/50"
}`}
>
{plan.popular && (
<Badge className="absolute -top-2.5 left-4">
Paling Populer
</Badge>
)}
<CardHeader>
<CardTitle>{plan.name}</CardTitle>
<div className="flex items-baseline gap-1">
<span className="text-xl font-bold text-foreground">
{plan.price}
</span>
{plan.priceSub && (
<span className="text-xs text-muted-foreground">
{plan.priceSub}
</span>
)}
</div>
{plan.description && (
<p className="text-xs text-muted-foreground mt-1">
{plan.description}
</p>
)}
</CardHeader>
<CardContent>
<ul className="space-y-2">
{plan.features.map((feature) => (
<li
key={feature}
className="flex items-center gap-2 text-sm text-muted-foreground"
>
<CheckCircle
className="size-3.5 text-primary shrink-0"
strokeWidth={2.5}
/>
<span>{feature}</span>
</li>
))}
</ul>
</CardContent>
<CardFooter>
<Button
variant={selected === plan.name ? "default" : "secondary"}
className="w-full"
size="lg"
>
{plan.buttonLabel}
</Button>
</CardFooter>
</Card>
))}
</div>
</div>
);
}

127
src/data/onboarding.json Normal file
View File

@ -0,0 +1,127 @@
{
"businessTypes": [
{
"label": "Restoran",
"image": "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=400&h=300&fit=crop"
},
{
"label": "Kafe",
"image": "https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=400&h=300&fit=crop"
},
{
"label": "Warung",
"image": "https://images.unsplash.com/photo-1604719312566-8912e9227c6a?w=400&h=300&fit=crop"
},
{
"label": "Minimarket",
"image": "https://images.unsplash.com/photo-1578916171728-46686eac8d58?w=400&h=300&fit=crop"
},
{
"label": "Toko Retail",
"image": "https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=400&h=300&fit=crop"
},
{
"label": "Jasa",
"image": "https://images.unsplash.com/photo-1497366216548-37526070297c?w=400&h=300&fit=crop"
},
{
"label": "Digital Product",
"image": "https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=400&h=300&fit=crop"
},
{
"label": "Distributor",
"image": "https://images.unsplash.com/photo-1553413077-190dd305871c?w=400&h=300&fit=crop"
},
{
"label": "Grosir",
"image": "https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=400&h=300&fit=crop"
},
{
"label": "Lainnya",
"image": "https://images.unsplash.com/photo-1556761175-4b46a572b786?w=400&h=300&fit=crop"
}
],
"plans": [
{
"name": "Starter",
"price": "Gratis",
"priceSub": "",
"description": "Cocok untuk bisnis baru.",
"features": [
"POS",
"Produk",
"Pelanggan",
"Laporan Dasar",
"Maksimal 100 Produk"
],
"buttonLabel": "Mulai Gratis",
"popular": false
},
{
"name": "Business",
"price": "Rp99.000",
"priceSub": "/bulan",
"description": "",
"features": [
"Semua Starter",
"Inventory",
"Multi Kasir",
"Diskon",
"Printer Struk",
"Laporan Lengkap",
"Export Excel"
],
"buttonLabel": "Pilih Paket",
"popular": true
},
{
"name": "Enterprise",
"price": "Hubungi Kami",
"priceSub": "",
"description": "",
"features": [
"Semua Business",
"Multi Cabang",
"Multi Gudang",
"Role Management",
"API Integration",
"Priority Support"
],
"buttonLabel": "Hubungi Sales",
"popular": false
}
],
"rightImages": {
"1": "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=800&h=1200&fit=crop",
"2": "https://images.unsplash.com/photo-1556742111-a301076d9d18?w=800&h=1200&fit=crop",
"3": "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=1200&fit=crop"
},
"stepLabels": {
"1": "Bisnis",
"2": "Info",
"3": "Paket"
},
"rightPanelContent": {
"1": {
"title": "Kelola bisnis lebih mudah bersama Profitra",
"description": "Pilih jenis usaha Anda dan mulai kelola bisnis dengan cara yang lebih modern."
},
"2": {
"title": "Semua data bisnis tersimpan dengan aman",
"description": "Informasi bisnis Anda dilengkapi dengan enkripsi dan perlindungan data terbaik."
},
"3": {
"title": "Siap meningkatkan bisnis Anda?",
"description": "Dengan paket yang tepat, bisnis Anda akan berkembang lebih cepat."
}
},
"currencies": [
{ "value": "IDR", "label": "Rupiah (IDR)" },
{ "value": "USD", "label": "Dollar (USD)" }
],
"timezones": [
{ "value": "Asia/Jakarta", "label": "Asia/Jakarta" },
{ "value": "Asia/Makassar", "label": "Asia/Makassar" },
{ "value": "Asia/Jayapura", "label": "Asia/Jayapura" }
]
}