fix and add som usecases
This commit is contained in:
@@ -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"
|
||||
|
||||
77
frontend/src/components/pages/danger-zone.tsx
Normal file
77
frontend/src/components/pages/danger-zone.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
82
frontend/src/components/pages/home-page.tsx
Normal file
82
frontend/src/components/pages/home-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
70
frontend/src/components/pages/login-page.tsx
Normal file
70
frontend/src/components/pages/login-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
53
frontend/src/components/pages/profile-editor.tsx
Normal file
53
frontend/src/components/pages/profile-editor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
51
frontend/src/components/pages/settings-panel.tsx
Normal file
51
frontend/src/components/pages/settings-panel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
14
frontend/src/components/protected-route.tsx
Normal file
14
frontend/src/components/protected-route.tsx
Normal 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
102
frontend/src/lib/auth.tsx
Normal 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;
|
||||
}
|
||||
6
frontend/src/lib/utils.ts
Normal file
6
frontend/src/lib/utils.ts
Normal 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))
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user