Initial commit

This commit is contained in:
Harivansh Rathi 2024-12-05 15:59:08 -05:00
commit ae239a2849
42 changed files with 6674 additions and 0 deletions

3
.bolt/config.json Normal file
View file

@ -0,0 +1,3 @@
{
"template": "bolt-vite-react-ts"
}

8
.bolt/prompt Normal file
View file

@ -0,0 +1,8 @@
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
Use icons from lucide-react for logos.
Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags.

View file

@ -0,0 +1,217 @@
# Technical Documentation
## Project Overview
This is a modern web application built as a study/learning platform with features for question asking, material management, and progress tracking.
## Optimized Project Structure
```
Root/
├── src/
│ ├── components/
│ │ ├── common/ # Shared components
│ │ │ ├── Button/
│ │ │ ├── Input/
│ │ │ └── Card/
│ │ ├── layout/ # Layout components
│ │ │ ├── Header/
│ │ │ ├── Sidebar/
│ │ │ └── DashboardLayout/
│ │ └── features/ # Feature-specific components
│ │ ├── auth/
│ │ ├── study/
│ │ └── dashboard/
│ ├── pages/ # Page components
│ │ ├── auth/ # Authentication pages
│ │ │ ├── Login.tsx
│ │ │ └── Signup.tsx
│ │ ├── dashboard/ # Dashboard pages
│ │ │ ├── index.tsx
│ │ │ ├── ask.tsx
│ │ │ ├── upload.tsx
│ │ │ ├── resources.tsx
│ │ │ ├── history.tsx
│ │ │ └── settings.tsx
│ │ └── Home.tsx
│ ├── routes/ # Routing configuration
│ │ ├── index.tsx # Root router
│ │ ├── PrivateRoute.tsx
│ │ └── routes.ts # Route definitions
│ ├── hooks/ # Custom hooks
│ ├── utils/ # Utility functions
│ ├── types/ # TypeScript types
│ ├── constants/ # Constants and configurations
│ ├── services/ # API services
│ ├── context/ # React context providers
│ ├── styles/ # Global styles
│ ├── App.tsx
│ └── main.tsx
├── public/
└── config/ # Configuration files
```
## Routing Implementation
### Route Structure
```typescript
routes/
├── index.tsx # Main router configuration
├── PrivateRoute.tsx # Protected route wrapper
└── routes.ts # Route definitions
// Route Configuration
const routes = {
public: {
home: '/',
login: '/auth/login',
signup: '/auth/signup',
},
private: {
dashboard: {
root: '/dashboard',
ask: '/dashboard/ask',
upload: '/dashboard/upload',
resources: '/dashboard/resources',
history: '/dashboard/history',
settings: '/dashboard/settings',
}
}
}
```
### Route Guards
- Implementation of protected routes using `PrivateRoute` component
- Authentication state management
- Role-based access control
## Component Architecture
### Layout Components
- **DashboardLayout**: HOC for dashboard pages with sidebar and header
- **AuthLayout**: HOC for authentication pages
- **PublicLayout**: HOC for public pages
### Feature Components
1. **Auth Module**
- LoginForm
- SignupForm
- PasswordReset
2. **Dashboard Module**
- QuestionForm
- MaterialUploader
- ResourceViewer
- HistoryTracker
3. **Common Components**
- Button
- Input
- Card
- Modal
- Loader
## State Management
- React Context for global state
- Custom hooks for shared logic
- Local component state for UI
## API Integration
- Axios for HTTP requests
- Service layer for API calls
- Request/Response interceptors
- Error handling middleware
## Error Handling
- Global error boundary
- Form validation
- API error handling
- User feedback system
## Performance Optimizations
- Route-based code splitting
- Lazy loading of components
- Memoization of expensive computations
- Image optimization
## Security Measures
- Protected routes
- JWT token management
- XSS prevention
- CSRF protection
- Input sanitization
## Testing Strategy
- Unit tests for utilities
- Component tests
- Integration tests
- E2E tests for critical flows
## Build Configuration
- Development and production builds
- Environment variables
- Bundle optimization
- Asset management
## Dependencies
### Core Dependencies
- React
- React DOM
- React Router
### UI Dependencies
- Radix UI components
- Lucide React icons
### Development Dependencies
- TypeScript
- ESLint
- Vite
- TailwindCSS
## Architecture
The application follows a modern React application architecture with:
- Modular and reusable components
- Centralized routing in App.tsx
- Tailwind CSS for styling
- TypeScript for type safety
- Separated configuration files
## Features
1. User Authentication
- Login
- Signup
- Session management
2. Study Tools
- Question submission
- Material uploads
- Resource management
- Progress tracking
3. User Settings
- Profile management
- Preferences
- History tracking

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

28
eslint.config.js Normal file
View file

@ -0,0 +1,28 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
);

13
index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5079
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

46
package.json Normal file
View file

@ -0,0 +1,46 @@
{
"name": "vite-react-typescript-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.22.3",
"tailwind-merge": "^2.2.1"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.11",
"globals": "^15.9.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.5.3",
"typescript-eslint": "^8.3.0",
"vite": "^5.4.2"
}
}

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

16
src/App.tsx Normal file
View file

@ -0,0 +1,16 @@
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { AppRouter } from './routes';
import './index.css';
const App: React.FC = () => {
return (
<BrowserRouter>
<div className="min-h-screen bg-background text-foreground">
<AppRouter />
</div>
</BrowserRouter>
);
};
export default App;

View file

@ -0,0 +1,33 @@
import React from 'react';
import { MessageSquare } from 'lucide-react';
interface Question {
id: string;
text: string;
timestamp: Date;
}
interface QuestionListProps {
questions: Question[];
}
export function QuestionList({ questions }: QuestionListProps) {
return (
<div className="space-y-4">
{questions.map((question) => (
<div
key={question.id}
className="flex items-start space-x-4 rounded-lg border p-4 hover:bg-muted/50"
>
<MessageSquare className="h-5 w-5 text-muted-foreground" />
<div className="flex-1">
<p className="font-medium">{question.text}</p>
<p className="text-sm text-muted-foreground">
{question.timestamp.toLocaleDateString()}
</p>
</div>
</div>
))}
</div>
);
}

View file

@ -0,0 +1,29 @@
import React from 'react'
import { Card } from '../ui/card'
interface StatsCardProps {
title: string
value: string
icon: React.ReactNode
trend: {
value: string
label: string
}
}
export function StatsCard({ title, value, icon, trend }: StatsCardProps) {
return (
<Card className="p-6">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-muted-foreground">{title}</h3>
{icon}
</div>
<div className="mt-2">
<p className="text-2xl font-bold">{value}</p>
<p className="text-sm text-muted-foreground">
<span className="text-green-600">{trend.value}</span> {trend.label}
</p>
</div>
</Card>
)
}

View file

@ -0,0 +1,16 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
import { Header } from '../Header';
const AuthLayout: React.FC = () => {
return (
<div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<Outlet />
</div>
</div>
);
};
export default AuthLayout;

View file

@ -0,0 +1,20 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
import { Header } from '../Header';
import { Sidebar } from '../Sidebar';
const DashboardLayout: React.FC = () => {
return (
<div className="flex min-h-screen flex-col">
<Header />
<div className="flex flex-1">
<Sidebar />
<main className="flex-1 overflow-y-auto bg-muted/50 p-6">
<Outlet />
</main>
</div>
</div>
);
};
export default DashboardLayout;

View file

@ -0,0 +1,28 @@
import React from 'react';
import { BookOpen } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '../../ui/Button';
export function Header() {
return (
<header className="border-b bg-background">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
<Link to="/" className="flex items-center gap-2 text-xl font-bold">
<BookOpen className="h-6 w-6 text-foreground" />
<span>StudyAI</span>
</Link>
<nav className="flex items-center gap-4">
<Link to="/dashboard">
<Button variant="ghost">Dashboard</Button>
</Link>
<Link to="/auth/login">
<Button variant="ghost">Log in</Button>
</Link>
<Link to="/auth/signup">
<Button>Sign up</Button>
</Link>
</nav>
</div>
</header>
);
}

View file

@ -0,0 +1,74 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import {
Settings,
MessageSquarePlus,
Upload,
BookOpen,
GraduationCap,
History
} from 'lucide-react';
import { cn } from '../../../lib/utils';
const sidebarItems = [
{
icon: MessageSquarePlus,
label: 'Ask a Question',
path: '/dashboard/ask'
},
{
icon: Upload,
label: 'Upload Materials',
path: '/dashboard/upload'
},
{
icon: BookOpen,
label: 'Study Resources',
path: '/dashboard/resources'
},
{
icon: History,
label: 'Study History',
path: '/dashboard/history'
},
{
icon: Settings,
label: 'Settings',
path: '/dashboard/settings'
},
];
export function Sidebar() {
const location = useLocation();
return (
<div className="flex h-full w-64 flex-col border-r bg-background">
<div className="flex h-14 items-center border-b px-4">
<GraduationCap className="h-6 w-6" />
<span className="ml-2 font-semibold">Study Dashboard</span>
</div>
<nav className="flex-1 space-y-1 px-2 py-4">
{sidebarItems.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.path;
return (
<Link
key={item.path}
to={item.path}
className={cn(
'flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
)}
>
<Icon className="mr-3 h-5 w-5" />
{item.label}
</Link>
);
})}
</nav>
</div>
);
}

View file

@ -0,0 +1,41 @@
import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
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-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'underline-offset-4 hover:underline text-primary',
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-3 rounded-md',
lg: 'h-11 px-8 rounded-md',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}

View file

@ -0,0 +1,16 @@
import React from 'react';
import { cn } from '../../lib/utils';
type CardProps = React.HTMLAttributes<HTMLDivElement>;
export function Card({ className, ...props }: CardProps) {
return (
<div
className={cn(
'rounded-lg border bg-card text-card-foreground shadow-sm',
className
)}
{...props}
/>
);
}

View file

@ -0,0 +1,23 @@
import React from 'react';
import { cn } from '../../lib/utils';
interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
orientation?: 'horizontal' | 'vertical';
}
export function Separator({
className,
orientation = 'horizontal',
...props
}: SeparatorProps) {
return (
<div
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{...props}
/>
);
}

59
src/index.css Normal file
View file

@ -0,0 +1,59 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

6
src/lib/utils.ts Normal file
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));
}

13
src/main.tsx Normal file
View file

@ -0,0 +1,13 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Root element not found');
createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>
);

72
src/pages/Home.tsx Normal file
View file

@ -0,0 +1,72 @@
import React from 'react';
import { ArrowRight, Brain, Sparkles, Users } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '../components/ui/Button';
import { Header } from '../components/layout/Header';
function Home() {
return (
<div>
<Header />
<main>
<section className="bg-muted/50 px-4 py-20">
<div className="mx-auto max-w-7xl text-center">
<h1 className="text-5xl font-bold tracking-tight">
Your AI Study Buddy
</h1>
<p className="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground">
Get instant help with your coursework using advanced AI. Upload your materials,
ask questions, and receive detailed explanations.
</p>
<div className="mt-10">
<Link to="/auth/signup">
<Button size="lg" className="gap-2">
Get Started <ArrowRight className="h-5 w-5" />
</Button>
</Link>
</div>
</div>
</section>
<section className="py-20">
<div className="mx-auto max-w-7xl px-4">
<h2 className="text-center text-3xl font-bold">Why Choose StudyAI?</h2>
<div className="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
{[
{
icon: Brain,
title: 'Smart Learning',
description:
'Our AI understands your questions and provides detailed, accurate answers.',
},
{
icon: Sparkles,
title: 'Instant Help',
description:
'Get immediate assistance with your coursework, available 24/7.',
},
{
icon: Users,
title: 'Personalized Experience',
description:
'The more you use StudyAI, the better it understands your learning style.',
},
].map((feature) => (
<div
key={feature.title}
className="rounded-xl border bg-card p-6 text-center shadow-sm"
>
<feature.icon className="mx-auto h-12 w-12 text-primary" />
<h3 className="mt-4 text-xl font-semibold">{feature.title}</h3>
<p className="mt-2 text-muted-foreground">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
</main>
</div>
);
}
export default Home;

76
src/pages/auth/Login.tsx Normal file
View file

@ -0,0 +1,76 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Button } from '../../components/ui/Button';
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Handle login
};
return (
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center bg-muted/50 py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight">
Sign in to your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4 rounded-md">
<div>
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="relative block w-full rounded-md border-0 py-1.5 px-3 bg-background text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6"
placeholder="Email address"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="relative block w-full rounded-md border-0 py-1.5 px-3 bg-background text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6"
placeholder="Password"
/>
</div>
</div>
<div>
<Button type="submit" className="w-full">
Sign in
</Button>
</div>
</form>
<p className="mt-2 text-center text-sm text-muted-foreground">
Don't have an account?{' '}
<Link to="/signup" className="font-medium text-primary hover:text-primary/90">
Sign up
</Link>
</p>
</div>
</div>
);
}
export default Login;

92
src/pages/auth/Signup.tsx Normal file
View file

@ -0,0 +1,92 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Button } from '../../components/ui/Button';
function Signup() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Handle signup
};
return (
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center bg-muted/50 py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight">
Create your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4 rounded-md">
<div>
<label htmlFor="name" className="sr-only">
Full name
</label>
<input
id="name"
name="name"
type="text"
required
value={name}
onChange={(e) => setName(e.target.value)}
className="relative block w-full rounded-md border-0 py-1.5 px-3 bg-background text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6"
placeholder="Full name"
/>
</div>
<div>
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="relative block w-full rounded-md border-0 py-1.5 px-3 bg-background text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6"
placeholder="Email address"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="relative block w-full rounded-md border-0 py-1.5 px-3 bg-background text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6"
placeholder="Password"
/>
</div>
</div>
<div>
<Button type="submit" className="w-full">
Sign up
</Button>
</div>
</form>
<p className="mt-2 text-center text-sm text-muted-foreground">
Already have an account?{' '}
<Link to="/login" className="font-medium text-primary hover:text-primary/90">
Sign in
</Link>
</p>
</div>
</div>
);
}
export default Signup;

View file

@ -0,0 +1,68 @@
import React from 'react';
import { Button } from '../../components/ui/Button';
import { Card } from '../../components/ui/card';
function Settings() {
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">Settings</h1>
<Card className="p-6">
<h2 className="text-xl font-semibold">Profile Settings</h2>
<div className="mt-4 space-y-4">
<div>
<label className="block text-sm font-medium text-foreground">
Display Name
</label>
<input
type="text"
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="Your name"
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground">
Email
</label>
<input
type="email"
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="your@email.com"
/>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-xl font-semibold">Notification Settings</h2>
<div className="mt-4 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium">Email Notifications</h3>
<p className="text-sm text-muted-foreground">
Receive updates about your study progress
</p>
</div>
<Button variant="outline">Configure</Button>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-xl font-semibold">Account Settings</h2>
<div className="mt-4 space-y-4">
<div>
<Button variant="outline" className="text-destructive">
Delete Account
</Button>
<p className="mt-2 text-sm text-muted-foreground">
This action cannot be undone.
</p>
</div>
</div>
</Card>
</div>
);
}
export default Settings;

View file

@ -0,0 +1,53 @@
import React, { useState } from 'react';
import { Send, Upload } from 'lucide-react';
import { Button } from '../../components/ui/Button';
function AskQuestion() {
const [question, setQuestion] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Handle question submission
};
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">Ask a Question</h1>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="rounded-lg bg-card p-6 shadow-sm">
<label className="block">
<span className="text-sm font-medium text-foreground">
What would you like to know?
</span>
<textarea
value={question}
onChange={(e) => setQuestion(e.target.value)}
className="mt-2 block w-full rounded-lg border border-input px-4 py-2 focus:border-primary focus:ring-primary"
rows={4}
placeholder="Type your question here..."
/>
</label>
</div>
<div className="rounded-lg bg-card p-6 shadow-sm">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-foreground">
Upload Study Materials (Optional)
</span>
<Button variant="secondary" type="button" className="gap-2">
<Upload className="h-4 w-4" />
Upload Files
</Button>
</div>
</div>
<Button type="submit" className="w-full gap-2">
<Send className="h-5 w-5" />
Submit Question
</Button>
</form>
</div>
);
}
export default AskQuestion;

View file

@ -0,0 +1,65 @@
import React from 'react';
import { Calendar } from 'lucide-react';
import { Card } from '../../components/ui/card';
const mockHistory = [
{
id: '1',
date: new Date('2024-03-10'),
activity: 'Studied Biology',
duration: '2 hours',
topics: ['Photosynthesis', 'Cell Structure'],
},
{
id: '2',
date: new Date('2024-03-09'),
activity: 'Practice Problems',
duration: '1.5 hours',
topics: ['Algebra', 'Calculus'],
},
{
id: '3',
date: new Date('2024-03-08'),
activity: 'Reading Assignment',
duration: '1 hour',
topics: ['Literature', 'Poetry Analysis'],
},
];
function StudyHistory() {
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">Study History</h1>
<div className="space-y-4">
{mockHistory.map((item) => (
<Card key={item.id} className="p-4">
<div className="flex items-start space-x-4">
<Calendar className="h-5 w-5 text-muted-foreground" />
<div className="flex-1">
<div className="flex items-center justify-between">
<h3 className="font-medium">{item.activity}</h3>
<span className="text-sm text-muted-foreground">
{item.date.toLocaleDateString()}
</span>
</div>
<p className="text-sm text-muted-foreground">Duration: {item.duration}</p>
<div className="mt-2 flex flex-wrap gap-2">
{item.topics.map((topic) => (
<span
key={topic}
className="rounded-full bg-primary/10 px-2 py-1 text-xs text-primary"
>
{topic}
</span>
))}
</div>
</div>
</div>
</Card>
))}
</div>
</div>
);
}
export default StudyHistory;

View file

@ -0,0 +1,73 @@
import React from 'react';
import { Users, CreditCard, Activity, PlusCircle } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '../../components/ui/Button';
import { QuestionList } from '../../components/dashboard/QuestionList';
import { StatsCard } from '../../components/dashboard/StatsCard';
import { Card } from '../../components/ui/card';
import { Separator } from '../../components/ui/separator';
const mockQuestions = [
{
id: '1',
text: 'How does photosynthesis work?',
timestamp: new Date('2024-03-10'),
},
{
id: '2',
text: 'Explain the concept of supply and demand in economics.',
timestamp: new Date('2024-03-09'),
},
];
function Dashboard() {
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">Dashboard</h1>
<Link to="/dashboard/ask">
<Button className="gap-2">
<PlusCircle className="h-5 w-5" />
Ask a Question
</Button>
</Link>
</div>
<div className="grid gap-4 md:grid-cols-3">
<StatsCard
title="Total Questions"
value="156"
icon={<Users className="h-4 w-4 text-muted-foreground" />}
trend={{ value: "+12%", label: "from last month" }}
/>
<StatsCard
title="Study Sessions"
value="32"
icon={<CreditCard className="h-4 w-4 text-muted-foreground" />}
trend={{ value: "+8", label: "from last week" }}
/>
<StatsCard
title="Active Streak"
value="7 days"
icon={<Activity className="h-4 w-4 text-muted-foreground" />}
trend={{ value: "+2", label: "days" }}
/>
</div>
<Card>
<div className="p-6">
<h2 className="text-lg font-semibold">Recent Questions</h2>
<p className="text-sm text-muted-foreground">
Your most recent study questions and their status.
</p>
</div>
<Separator />
<div className="p-6">
<QuestionList questions={mockQuestions} />
</div>
</Card>
</div>
);
}
export default Dashboard;

View file

@ -0,0 +1,63 @@
import React from 'react';
import { Book, File, Folder } from 'lucide-react';
import { Card } from '../../components/ui/card';
const mockResources = [
{
id: '1',
type: 'folder',
name: 'Biology',
itemCount: 12,
},
{
id: '2',
type: 'folder',
name: 'Physics',
itemCount: 8,
},
{
id: '3',
type: 'file',
name: 'Math Notes.pdf',
size: '2.4 MB',
},
{
id: '4',
type: 'book',
name: 'Chemistry Textbook',
author: 'John Smith',
},
];
function StudyResources() {
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">Study Resources</h1>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{mockResources.map((resource) => (
<Card key={resource.id} className="p-4 hover:bg-muted/50 cursor-pointer">
<div className="flex items-start space-x-4">
{resource.type === 'folder' && <Folder className="h-6 w-6 text-blue-500" />}
{resource.type === 'file' && <File className="h-6 w-6 text-green-500" />}
{resource.type === 'book' && <Book className="h-6 w-6 text-purple-500" />}
<div>
<h3 className="font-medium">{resource.name}</h3>
{resource.type === 'folder' && (
<p className="text-sm text-muted-foreground">{resource.itemCount} items</p>
)}
{resource.type === 'file' && (
<p className="text-sm text-muted-foreground">{resource.size}</p>
)}
{resource.type === 'book' && (
<p className="text-sm text-muted-foreground">By {resource.author}</p>
)}
</div>
</div>
</Card>
))}
</div>
</div>
);
}
export default StudyResources;

View file

@ -0,0 +1,25 @@
import React from 'react';
import { Upload } from 'lucide-react';
import { Button } from '../../components/ui/Button';
function UploadMaterials() {
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">Upload Study Materials</h1>
<div className="rounded-lg bg-card p-6 shadow-sm">
<div className="flex flex-col items-center justify-center space-y-4 py-12">
<Upload className="h-12 w-12 text-muted-foreground" />
<div className="text-center">
<h2 className="text-lg font-semibold">Drop your files here</h2>
<p className="text-sm text-muted-foreground">
or click to browse from your computer
</p>
</div>
<Button variant="outline">Choose Files</Button>
</div>
</div>
</div>
);
}
export default UploadMaterials;

View file

@ -0,0 +1,25 @@
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { routes } from './routes';
// TODO: Replace with your actual auth hook
const useAuth = () => {
return {
isAuthenticated: true, // Temporarily set to true for development
};
};
interface PrivateRouteProps {
children: React.ReactNode;
}
export function PrivateRoute({ children }: PrivateRouteProps) {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to={routes.public.login} state={{ from: location }} replace />;
}
return <>{children}</>;
}

73
src/routes/index.tsx Normal file
View file

@ -0,0 +1,73 @@
import React, { Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { routes } from './routes';
import { PrivateRoute } from './PrivateRoute';
// Loading component
const Loading: React.FC = () => (
<div className="flex h-screen items-center justify-center">
<div className="text-lg">Loading...</div>
</div>
);
// Layouts
const DashboardLayout = React.lazy(() => import('../components/layout/DashboardLayout'));
const AuthLayout = React.lazy(() => import('../components/layout/AuthLayout'));
// Public Pages
const Home = React.lazy(() => import('../pages/Home'));
const Login = React.lazy(() => import('../pages/auth/Login'));
const Signup = React.lazy(() => import('../pages/auth/Signup'));
// Dashboard Pages
const Dashboard = React.lazy(() => import('../pages/dashboard'));
const AskQuestion = React.lazy(() => import('../pages/dashboard/ask'));
const UploadMaterials = React.lazy(() => import('../pages/dashboard/upload'));
const StudyResources = React.lazy(() => import('../pages/dashboard/resources'));
const StudyHistory = React.lazy(() => import('../pages/dashboard/history'));
const Settings = React.lazy(() => import('../pages/dashboard/Settings'));
const withSuspense = (Component: React.LazyExoticComponent<React.ComponentType<object>>) => {
return (
<Suspense fallback={<Loading />}>
<Component />
</Suspense>
);
};
export const AppRouter: React.FC = () => {
return (
<Suspense fallback={<Loading />}>
<Routes>
{/* Public Routes */}
<Route path={routes.public.home} element={withSuspense(Home)} />
{/* Auth Routes */}
<Route path="/auth" element={withSuspense(AuthLayout)}>
<Route path="login" element={withSuspense(Login)} />
<Route path="signup" element={withSuspense(Signup)} />
</Route>
{/* Protected Dashboard Routes */}
<Route
path="/dashboard"
element={
<PrivateRoute>
{withSuspense(DashboardLayout)}
</PrivateRoute>
}
>
<Route index element={withSuspense(Dashboard)} />
<Route path="ask" element={withSuspense(AskQuestion)} />
<Route path="upload" element={withSuspense(UploadMaterials)} />
<Route path="resources" element={withSuspense(StudyResources)} />
<Route path="history" element={withSuspense(StudyHistory)} />
<Route path="settings" element={withSuspense(Settings)} />
</Route>
{/* Catch-all route */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
);
};

21
src/routes/routes.ts Normal file
View file

@ -0,0 +1,21 @@
export const routes = {
public: {
home: '/',
login: '/auth/login',
signup: '/auth/signup',
},
private: {
dashboard: {
root: '/dashboard',
ask: '/dashboard/ask',
upload: '/dashboard/upload',
resources: '/dashboard/resources',
history: '/dashboard/history',
settings: '/dashboard/settings',
}
}
} as const;
export type Routes = typeof routes;
export type PublicRoutes = keyof typeof routes.public;
export type PrivateRoutes = keyof typeof routes.private.dashboard;

11
src/types/index.ts Normal file
View file

@ -0,0 +1,11 @@
export interface Question {
id: string;
text: string;
timestamp: Date;
answer?: string;
}
export interface User {
name: string;
email: string;
}

26
src/utils/cn.ts Normal file
View file

@ -0,0 +1,26 @@
type ClassValue = string | number | boolean | undefined | null;
type ClassArray = ClassValue[];
type ClassObject = { [key: string]: any };
type ClassInput = ClassValue | ClassArray | ClassObject;
export function cn(...inputs: ClassInput[]): string {
const classes = [];
for (const input of inputs) {
if (!input) continue;
if (typeof input === 'string') {
classes.push(input);
} else if (Array.isArray(input)) {
classes.push(cn(...input));
} else if (typeof input === 'object') {
for (const key in input) {
if (input[key]) {
classes.push(key);
}
}
}
}
return classes.join(' ');
}

1
src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

63
tailwind.config.js Normal file
View file

@ -0,0 +1,63 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(0 0% 20%)",
input: "hsl(0 0% 20%)",
ring: "hsl(0 0% 20%)",
background: "hsl(0 0% 100%)",
foreground: "hsl(0 0% 0%)",
primary: {
DEFAULT: "hsl(0 0% 0%)",
foreground: "hsl(0 0% 100%)",
},
secondary: {
DEFAULT: "hsl(0 0% 96%)",
foreground: "hsl(0 0% 0%)",
},
destructive: {
DEFAULT: "hsl(0 0% 0%)",
foreground: "hsl(0 0% 100%)",
},
muted: {
DEFAULT: "hsl(0 0% 96%)",
foreground: "hsl(0 0% 40%)",
},
accent: {
DEFAULT: "hsl(0 0% 96%)",
foreground: "hsl(0 0% 0%)",
},
popover: {
DEFAULT: "hsl(0 0% 100%)",
foreground: "hsl(0 0% 0%)",
},
card: {
DEFAULT: "hsl(0 0% 100%)",
foreground: "hsl(0 0% 0%)",
},
},
borderRadius: {
lg: "0.5rem",
md: "calc(0.5rem - 2px)",
sm: "calc(0.5rem - 4px)",
},
boxShadow: {
sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
DEFAULT: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
},
},
},
plugins: [],
};

30
tsconfig.app.json Normal file
View file

@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}

7
tsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

22
tsconfig.node.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

10
vite.config.ts Normal file
View file

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
optimizeDeps: {
exclude: ['lucide-react'],
},
});