fix and add som usecases

This commit is contained in:
2025-09-15 17:41:07 +08:00
parent 838806b9e3
commit 414737fd48
37 changed files with 1133 additions and 51 deletions

View File

@@ -8,13 +8,16 @@ import {
import { FormElementsPage } from "./components/pages/form-elements";
import { DynamicContentPage } from "./components/pages/dynamic-content";
import { cn } from "./lib/utils";
import { LoginPage } from "./components/pages/login-page";
import { HomePage } from "./components/pages/home-page";
import { ProtectedRoute } from "./components/protected-route";
function App() {
return (
<Router>
<div className="flex h-screen bg-gray-100">
<div className="flex h-screen bg-background text-foreground">
{/* Sidebar */}
<aside className="w-64 bg-white border-r">
<aside className="w-64 bg-card border-r border-border">
<div className="p-4">
<Link to="/">
<h1 className="text-2xl font-bold">Test App</h1>
@@ -22,13 +25,26 @@ function App() {
</div>
<nav className="mt-4">
<ul>
<li>
<NavLink
to="/home"
className={({ isActive }) =>
cn(
"block px-4 py-2 hover:bg-accent",
isActive && "bg-muted"
)
}
>
Home
</NavLink>
</li>
<li>
<NavLink
to="/form-elements"
className={({ isActive }) =>
cn(
"block px-4 py-2 text-gray-700 hover:bg-gray-200",
isActive && "bg-gray-300"
"block px-4 py-2 hover:bg-accent",
isActive && "bg-muted"
)
}
>
@@ -40,14 +56,27 @@ function App() {
to="/dynamic-content"
className={({ isActive }) =>
cn(
"block px-4 py-2 text-gray-700 hover:bg-gray-200",
isActive && "bg-gray-300"
"block px-4 py-2 hover:bg-accent",
isActive && "bg-muted"
)
}
>
Dynamic Content
</NavLink>
</li>
<li>
<NavLink
to="/login"
className={({ isActive }) =>
cn(
"block px-4 py-2 hover:bg-accent",
isActive && "bg-muted"
)
}
>
Login Page
</NavLink>
</li>
</ul>
</nav>
</aside>
@@ -55,6 +84,15 @@ function App() {
{/* Main Content */}
<main className="flex-1 overflow-y-auto">
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/home"
element={
<ProtectedRoute>
<HomePage />
</ProtectedRoute>
}
/>
<Route path="/form-elements" element={<FormElementsPage />} />
<Route
path="/dynamic-content"

View File

@@ -0,0 +1,77 @@
import { useState } from 'react';
import { useAuth } from '@/lib/auth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
export function DangerZone() {
const { logout } = useAuth();
const [confirmationText, setConfirmationText] = useState('');
const [isDialogOpen, setIsDialogOpen] = useState(false);
const requiredText = 'delete my account';
const handleDelete = () => {
if (confirmationText === requiredText) {
console.log('Account deletion confirmed.');
// In a real app, you'd make an API call here.
// For now, we just log out.
setIsDialogOpen(false);
logout();
}
};
return (
<Card className="border-red-500">
<CardHeader>
<CardTitle className="text-red-600">Danger Zone</CardTitle>
<CardDescription>These actions are permanent and cannot be undone.</CardDescription>
</CardHeader>
<CardContent className="flex justify-between items-center">
<p className="font-medium">Delete this account</p>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button variant="destructive">Delete Account</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="confirmation">
Please type <span className="font-bold text-gray-800">{requiredText}</span> to confirm.
</Label>
<Input
id="confirmation"
value={confirmationText}
onChange={(e) => setConfirmationText(e.target.value)}
/>
</div>
<DialogFooter>
<Button
variant="destructive"
onClick={handleDelete}
disabled={confirmationText !== requiredText}
>
I understand, delete my account
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,82 @@
import { useAuth } from '@/lib/auth';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ProfileEditor } from './profile-editor';
import { SettingsPanel } from './settings-panel';
import { DangerZone } from './danger-zone';
import { Button } from '../ui/button';
import { useNavigate } from 'react-router-dom';
export function HomePage() {
const { user, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate('/login');
};
return (
<div className="p-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Welcome, {user?.name}!</h1>
<Button onClick={handleLogout} variant="outline">Logout</Button>
</div>
<Tabs defaultValue="dashboard" className="space-y-4">
<TabsList>
<TabsTrigger value="dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="dashboard" className="space-y-4">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>Test Cases</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">142</p>
<p className="text-xs text-muted-foreground">+5 since last week</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc pl-5 text-sm">
<li>Ran 'test_login_flow'</li>
<li>Updated 'dynamic-content' page</li>
<li>Added 3 new form elements</li>
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Quick Links</CardTitle>
</CardHeader>
<CardContent className="flex flex-col space-y-2">
<a href="/form-elements" className="text-blue-600 hover:underline">Form Elements</a>
<a href="/dynamic-content" className="text-blue-600 hover:underline">Dynamic Content</a>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="profile">
<ProfileEditor />
</TabsContent>
<TabsContent value="settings">
<SettingsPanel />
</TabsContent>
</Tabs>
<div className="mt-12">
<DangerZone />
</div>
</div>
);
}

View File

@@ -0,0 +1,70 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useAuth } from '@/lib/auth';
export function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const { login } = useAuth();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const success = login(email, password);
if (success) {
navigate('/home');
} else {
setError('Invalid email or password');
}
};
return (
<div className="flex items-center justify-center min-h-screen">
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
Enter your email below to login to your account.
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}
</CardContent>
<CardFooter>
<Button type="submit" className="w-full">
Sign in
</Button>
</CardFooter>
</form>
</Card>
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { useState, FormEvent } from 'react';
import { useAuth } from '@/lib/auth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
export function ProfileEditor() {
const { user, updateUserInfo } = useAuth();
const [name, setName] = useState(user?.name || '');
const [feedback, setFeedback] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (name) {
updateUserInfo(name);
setFeedback('Profile updated successfully!');
setTimeout(() => setFeedback(''), 3000); // Clear feedback after 3 seconds
}
};
return (
<Card>
<CardHeader>
<CardTitle>Personal Information</CardTitle>
<CardDescription>Update your name here. Email is read-only.</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" value={user?.email || ''} disabled />
</div>
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your Name"
required
/>
</div>
</CardContent>
<CardFooter className="flex justify-between items-center">
<Button type="submit">Save Changes</Button>
{feedback && <p className="text-sm text-green-600">{feedback}</p>}
</CardFooter>
</form>
</Card>
);
}

View File

@@ -0,0 +1,51 @@
import { useAuth } from '@/lib/auth';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
export function SettingsPanel() {
const { user, updateSettings } = useAuth();
if (!user) {
return null; // Or a loading state
}
return (
<Card>
<CardHeader>
<CardTitle>Application Settings</CardTitle>
<CardDescription>Manage your theme and notification preferences.</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<Label htmlFor="notifications" className="font-medium">
Enable Notifications
</Label>
<Switch
id="notifications"
checked={user.notifications}
onCheckedChange={(checked) => updateSettings({ notifications: checked })}
/>
</div>
<div className="space-y-3">
<Label className="font-medium">Theme</Label>
<RadioGroup
value={user.theme}
onValueChange={(value: 'light' | 'dark') => updateSettings({ theme: value })}
className="flex space-x-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="light" id="light" />
<Label htmlFor="light">Light</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="dark" id="dark" />
<Label htmlFor="dark">Dark</Label>
</div>
</RadioGroup>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,14 @@
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/lib/auth';
export function ProtectedRoute({ children }: { children: JSX.Element }) {
const { user } = useAuth();
if (!user) {
// User is not authenticated
return <Navigate to="/login" />;
}
return children;
}

102
frontend/src/lib/auth.tsx Normal file
View File

@@ -0,0 +1,102 @@
import { createContext, useContext, useState, ReactNode, useEffect } from 'react';
// Define the shape of the user object
interface User {
email: string;
name: string;
theme: 'light' | 'dark';
notifications: boolean;
}
// Define the shape of the context
interface AuthContextType {
user: User | null;
login: (email: string, pass:string) => boolean;
logout: () => void;
updateUserInfo: (name: string) => void;
updateSettings: (settings: Partial<{ theme: 'light' | 'dark'; notifications: boolean }>) => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Key for sessionStorage
const USER_SESSION_KEY = 'test_app_user_session';
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(() => {
// Initialize state from sessionStorage on component mount
try {
const storedUser = sessionStorage.getItem(USER_SESSION_KEY);
return storedUser ? JSON.parse(storedUser) : null;
} catch (error) {
console.error("Failed to parse user from sessionStorage", error);
return null;
}
});
// Effect to update sessionStorage and theme class whenever user state changes
useEffect(() => {
const root = window.document.documentElement;
try {
if (user) {
sessionStorage.setItem(USER_SESSION_KEY, JSON.stringify(user));
// Apply theme
root.classList.remove('light', 'dark');
root.classList.add(user.theme);
} else {
sessionStorage.removeItem(USER_SESSION_KEY);
root.classList.remove('dark'); // Default to light theme on logout
}
} catch (error) {
console.error("Failed to save user to sessionStorage", error);
}
}, [user]);
// Hardcoded credentials for simulation
const login = (email: string, pass: string) => {
if (email === 'poop@shenjianl.cn' && pass === 'shenjianZ') {
// On login, set the full user object with defaults
setUser({
email,
name: 'Test User',
theme: 'light',
notifications: true,
});
return true;
}
return false;
};
const logout = () => {
setUser(null);
};
const updateUserInfo = (name: string) => {
setUser(currentUser => {
if (!currentUser) return null;
return { ...currentUser, name };
});
};
const updateSettings = (settings: Partial<{ theme: 'light' | 'dark'; notifications: boolean }>) => {
setUser(currentUser => {
if (!currentUser) return null;
return { ...currentUser, ...settings };
});
};
return (
<AuthContext.Provider value={{ user, login, logout, updateUserInfo, updateSettings }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { AuthProvider } from './lib/auth.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<AuthProvider>
<App />
</AuthProvider>
</StrictMode>,
)

0
frontend/theme.md Normal file
View File