From 62f45ed45492bc50974c9aaf1ed10d1d3ecf1f37 Mon Sep 17 00:00:00 2001
From: Asep Haryana Saputra <90584806+MythEclipse@users.noreply.github.com>
Date: Fri, 22 May 2026 12:41:55 +0000
Subject: [PATCH] Add React web scaffold
---
apps/web/index.html | 12 ++++
apps/web/moon.yml | 35 +++++++++++
apps/web/package.json | 33 ++++++++++
apps/web/postcss.config.js | 6 ++
apps/web/src/app.tsx | 25 ++++++++
apps/web/src/components/ui/button.tsx | 36 +++++++++++
apps/web/src/components/ui/card.tsx | 22 +++++++
apps/web/src/index.css | 27 +++++++++
apps/web/src/lib/utils.ts | 6 ++
apps/web/src/main.tsx | 10 +++
apps/web/src/pages/dashboard-page.tsx | 72 ++++++++++++++++++++++
apps/web/src/pages/landing-page.tsx | 87 +++++++++++++++++++++++++++
apps/web/src/store/ui-store.ts | 12 ++++
apps/web/tailwind.config.ts | 33 ++++++++++
apps/web/tsconfig.json | 14 +++++
apps/web/tsconfig.node.json | 10 +++
apps/web/vite.config.ts | 12 ++++
17 files changed, 452 insertions(+)
create mode 100644 apps/web/index.html
create mode 100644 apps/web/moon.yml
create mode 100644 apps/web/package.json
create mode 100644 apps/web/postcss.config.js
create mode 100644 apps/web/src/app.tsx
create mode 100644 apps/web/src/components/ui/button.tsx
create mode 100644 apps/web/src/components/ui/card.tsx
create mode 100644 apps/web/src/index.css
create mode 100644 apps/web/src/lib/utils.ts
create mode 100644 apps/web/src/main.tsx
create mode 100644 apps/web/src/pages/dashboard-page.tsx
create mode 100644 apps/web/src/pages/landing-page.tsx
create mode 100644 apps/web/src/store/ui-store.ts
create mode 100644 apps/web/tailwind.config.ts
create mode 100644 apps/web/tsconfig.json
create mode 100644 apps/web/tsconfig.node.json
create mode 100644 apps/web/vite.config.ts
diff --git a/apps/web/index.html b/apps/web/index.html
new file mode 100644
index 0000000..2d65458
--- /dev/null
+++ b/apps/web/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ ZeaVis Edu
+
+
+
+
+
+
diff --git a/apps/web/moon.yml b/apps/web/moon.yml
new file mode 100644
index 0000000..d8bc7f8
--- /dev/null
+++ b/apps/web/moon.yml
@@ -0,0 +1,35 @@
+type: application
+language: typescript
+
+tasks:
+ dev:
+ command: bun run dev
+ local: true
+ build:
+ command: bun run build
+ deps:
+ - shared:build
+ inputs:
+ - src/**/*
+ - index.html
+ - package.json
+ - tsconfig.json
+ - tsconfig.node.json
+ - vite.config.ts
+ - tailwind.config.ts
+ - postcss.config.js
+ - ../../packages/shared/dist/**/*
+ outputs:
+ - dist
+ typecheck:
+ command: bun run typecheck
+ deps:
+ - shared:typecheck
+ inputs:
+ - src/**/*
+ - package.json
+ - tsconfig.json
+ - tsconfig.node.json
+ - vite.config.ts
+ - tailwind.config.ts
+ - ../../packages/shared/dist/**/*
diff --git a/apps/web/package.json b/apps/web/package.json
new file mode 100644
index 0000000..a4b0feb
--- /dev/null
+++ b/apps/web/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@zeavis/web",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "build": "tsc --project tsconfig.json && vite build",
+ "preview": "vite preview --host 0.0.0.0",
+ "typecheck": "tsc --project tsconfig.json"
+ },
+ "dependencies": {
+ "@radix-ui/react-slot": "^1.1.0",
+ "@tanstack/react-query": "^5.59.16",
+ "@vitejs/plugin-react": "^4.3.3",
+ "@zeavis/shared": "workspace:*",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.468.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0",
+ "tailwind-merge": "^2.5.4",
+ "zustand": "^5.0.1"
+ },
+ "devDependencies": {
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.15",
+ "typescript": "^5.6.3",
+ "vite": "^6.0.1"
+ }
+}
diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js
new file mode 100644
index 0000000..2aa7205
--- /dev/null
+++ b/apps/web/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx
new file mode 100644
index 0000000..e66e83a
--- /dev/null
+++ b/apps/web/src/app.tsx
@@ -0,0 +1,25 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { createBrowserRouter, RouterProvider } from 'react-router-dom';
+import { DashboardPage } from '@/pages/dashboard-page';
+import { LandingPage } from '@/pages/landing-page';
+
+const queryClient = new QueryClient();
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ },
+ {
+ path: '/dashboard',
+ element: ,
+ },
+]);
+
+export function App() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
new file mode 100644
index 0000000..b0ac053
--- /dev/null
+++ b/apps/web/src/components/ui/button.tsx
@@ -0,0 +1,36 @@
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+import type { ButtonHTMLAttributes } from 'react';
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ outline: 'border border-border bg-transparent hover:bg-muted',
+ ghost: 'hover:bg-muted',
+ },
+ size: {
+ default: 'h-10 px-4 py-2',
+ lg: 'h-12 rounded-lg px-6',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+type ButtonProps = ButtonHTMLAttributes &
+ VariantProps & {
+ asChild?: boolean;
+ };
+
+export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
+ const Comp = asChild ? Slot : 'button';
+
+ return ;
+}
diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx
new file mode 100644
index 0000000..dae5948
--- /dev/null
+++ b/apps/web/src/components/ui/card.tsx
@@ -0,0 +1,22 @@
+import type { HTMLAttributes } from 'react';
+import { cn } from '@/lib/utils';
+
+export function Card({ className, ...props }: HTMLAttributes) {
+ return ;
+}
+
+export function CardHeader({ className, ...props }: HTMLAttributes) {
+ return ;
+}
+
+export function CardTitle({ className, ...props }: HTMLAttributes) {
+ return ;
+}
+
+export function CardDescription({ className, ...props }: HTMLAttributes) {
+ return ;
+}
+
+export function CardContent({ className, ...props }: HTMLAttributes) {
+ return ;
+}
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
new file mode 100644
index 0000000..21285dc
--- /dev/null
+++ b/apps/web/src/index.css
@@ -0,0 +1,27 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 48 60% 97%;
+ --foreground: 156 26% 12%;
+ --card: 0 0% 100%;
+ --card-foreground: 156 26% 12%;
+ --primary: 142 60% 32%;
+ --primary-foreground: 0 0% 100%;
+ --muted: 84 35% 90%;
+ --muted-foreground: 156 12% 35%;
+ --border: 84 24% 82%;
+ --radius: 1rem;
+ }
+
+ * {
+ @apply border-border;
+ }
+
+ body {
+ @apply bg-background text-foreground antialiased;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ }
+}
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
new file mode 100644
index 0000000..2819a83
--- /dev/null
+++ b/apps/web/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
new file mode 100644
index 0000000..81819a4
--- /dev/null
+++ b/apps/web/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { App } from './app';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/apps/web/src/pages/dashboard-page.tsx b/apps/web/src/pages/dashboard-page.tsx
new file mode 100644
index 0000000..273f696
--- /dev/null
+++ b/apps/web/src/pages/dashboard-page.tsx
@@ -0,0 +1,72 @@
+import { BookOpen, History, Leaf, LayoutDashboard } from 'lucide-react';
+import { Link } from 'react-router-dom';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { useUiStore } from '@/store/ui-store';
+
+const dashboardCards = [
+ {
+ icon: Leaf,
+ title: 'Deteksi Penyakit',
+ description: 'Area ini akan menjadi pintu masuk analisis gambar daun jagung.',
+ },
+ {
+ icon: History,
+ title: 'Riwayat Analisis',
+ description: 'Hasil deteksi sebelumnya akan ditampilkan saat fitur data tersedia.',
+ },
+ {
+ icon: BookOpen,
+ title: 'Materi Edukasi',
+ description: 'Konten edukasi penyakit jagung akan terhubung ke modul pembelajaran.',
+ },
+];
+
+export function DashboardPage() {
+ const { dashboardCompact, toggleDashboardCompact } = useUiStore();
+
+ return (
+
+
+
+
+
+ Dashboard
+
+
ZeaVis Edu Workspace
+
+ Placeholder awal untuk fitur deteksi, riwayat analisis, dan edukasi.
+
+
+
+
+
+
+
+
+
+ {dashboardCards.map((item) => (
+
+
+
+
+
+ {item.title}
+ {item.description}
+
+
+
+ Belum ada data. Fitur akan dihubungkan pada iterasi berikutnya.
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/landing-page.tsx b/apps/web/src/pages/landing-page.tsx
new file mode 100644
index 0000000..faba059
--- /dev/null
+++ b/apps/web/src/pages/landing-page.tsx
@@ -0,0 +1,87 @@
+import { ArrowRight, Leaf, ShieldCheck, Sprout } from 'lucide-react';
+import { Link } from 'react-router-dom';
+import { Button } from '@/components/ui/button';
+import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+
+const features = [
+ {
+ icon: Leaf,
+ title: 'Deteksi penyakit daun jagung',
+ description: 'Fondasi aplikasi siap untuk integrasi model klasifikasi ZeaVis Edu.',
+ },
+ {
+ icon: ShieldCheck,
+ title: 'Edukasi berbasis data',
+ description: 'Materi dan hasil analisis dapat dikembangkan di atas dashboard awal.',
+ },
+ {
+ icon: Sprout,
+ title: 'Siap tumbuh bersama produk',
+ description: 'Monorepo memisahkan frontend, backend, dan shared types dengan jelas.',
+ },
+];
+
+export function LandingPage() {
+ return (
+
+
+
+
+
+
+
+ Platform edukasi kesehatan tanaman jagung
+
+
+
+ Belajar mengenali penyakit daun jagung dengan alur digital yang rapi.
+
+
+ Scaffold ini menyiapkan fondasi aplikasi ZeaVis Edu untuk antarmuka edukasi,
+ API, dan integrasi model machine learning berikutnya.
+
+
+
+
+
+
+ {features.map((feature) => (
+
+
+
+
+
+
+ {feature.title}
+ {feature.description}
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/store/ui-store.ts b/apps/web/src/store/ui-store.ts
new file mode 100644
index 0000000..8f0fcfa
--- /dev/null
+++ b/apps/web/src/store/ui-store.ts
@@ -0,0 +1,12 @@
+import { create } from 'zustand';
+
+type UiState = {
+ dashboardCompact: boolean;
+ toggleDashboardCompact: () => void;
+};
+
+export const useUiStore = create((set) => ({
+ dashboardCompact: false,
+ toggleDashboardCompact: () =>
+ set((state) => ({ dashboardCompact: !state.dashboardCompact })),
+}));
diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts
new file mode 100644
index 0000000..88cb33f
--- /dev/null
+++ b/apps/web/tailwind.config.ts
@@ -0,0 +1,33 @@
+import type { Config } from 'tailwindcss';
+
+export default {
+ darkMode: ['class'],
+ content: ['./index.html', './src/**/*.{ts,tsx}'],
+ theme: {
+ extend: {
+ colors: {
+ border: 'hsl(var(--border))',
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))',
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))',
+ },
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))',
+ },
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
+ },
+ },
+ plugins: [],
+} satisfies Config;
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
new file mode 100644
index 0000000..14cbdd2
--- /dev/null
+++ b/apps/web/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "types": ["vite/client"],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"],
+ "@zeavis/shared": ["../../packages/shared/src/index.ts"],
+ "@zeavis/shared/*": ["../../packages/shared/src/*"]
+ }
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json
new file mode 100644
index 0000000..3710a09
--- /dev/null
+++ b/apps/web/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "types": ["node"]
+ },
+ "include": ["vite.config.ts", "tailwind.config.ts"]
+}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
new file mode 100644
index 0000000..93cad99
--- /dev/null
+++ b/apps/web/vite.config.ts
@@ -0,0 +1,12 @@
+import react from '@vitejs/plugin-react';
+import path from 'node:path';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+});