-
Notifications
You must be signed in to change notification settings - Fork 19
Getting Started Frontend
Welcome frontend developers! Let's build interfaces that work everywhere - from modern browsers to 2G connections.
As a frontend developer for EduLite, you'll create:
- 📱 Responsive interfaces that work on any device
- ⚡ Lightning-fast pages optimized for slow networks
- 🌍 Multi-language support for global accessibility
- 🌙 Dark mode for comfortable studying
- ♾️ Lightweight features for unreliable connections
- HTML/CSS basics (elements, styling, flexbox)
- JavaScript fundamentals (functions, arrays, objects)
- Git basics (clone, commit, push)
- React experience (we'll teach you!)
- Tailwind CSS knowledge
- Component-based thinking
- Accessibility awareness
Already did the Quick Start? Skip to Understanding the Architecture!
Need setup? Follow these steps:
# 1. Clone your fork
git clone https://github.com/YOUR_USERNAME/EduLite.git
cd EduLite/Frontend/EduLiteFrontend
# 2. Install dependencies
npm install
# 3. Start development server
npm run devVisit http://localhost:5173/ - You should see EduLite!
Frontend/EduLiteFrontend/
├── src/
│ ├── components/ # Reusable UI components
│ │ ├── Navbar.jsx # Navigation bar
│ │ ├── DarkModeToggle.jsx # Theme switcher
│ │ ├── LanguageSwitcher.jsx
│ │ └── Sidebar.jsx # Mobile menu
│ ├── pages/ # Full page components
│ │ ├── Home.jsx # Landing page
│ │ ├── About.jsx # About EduLite
│ │ └── Chats.jsx # Messaging
│ ├── i18n/ # Translations
│ │ ├── locales/
│ │ │ ├── en.json # English
│ │ │ └── ar.json # Arabic
│ │ └── index.js # i18n setup
│ ├── assets/ # Images, logos
│ ├── App.jsx # Main app component
│ └── main.jsx # Entry point
├── public/ # Static files
├── index.html # HTML template
└── vite.config.js # Build configuration
- React: Component-based UI library
- Vite: Super fast build tool (way faster than webpack!)
- Utility-first CSS framework
- No more naming classes!
- Responsive by default
- Client-side routing
- No page reloads = faster experience
- Internationalization (i18n)
- Easy translation management
Let's create a simple timer to help students track study sessions!
File: src/components/StudyTimer.jsx
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
function StudyTimer() {
const { t } = useTranslation();
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
} else if (!isActive && seconds !== 0) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [isActive, seconds]);
const toggle = () => {
setIsActive(!isActive);
};
const reset = () => {
setSeconds(0);
setIsActive(false);
};
const formatTime = (totalSeconds) => {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
return `${hours.toString().padStart(2, '0')}:
${minutes.toString().padStart(2, '0')}:
${secs.toString().padStart(2, '0')}`;
};
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-white">
{t('studyTimer.title', 'Study Timer')}
</h3>
<div className="text-4xl font-mono text-center mb-6 text-blue-600 dark:text-blue-400">
{formatTime(seconds)}
</div>
<div className="flex gap-2 justify-center">
<button
onClick={toggle}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600
transition-colors duration-200"
>
{isActive ? t('studyTimer.pause', 'Pause') : t('studyTimer.start', 'Start')}
</button>
<button
onClick={reset}
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600
transition-colors duration-200"
>
{t('studyTimer.reset', 'Reset')}
</button>
</div>
</div>
);
}
export default StudyTimer;File: src/i18n/locales/en.json
Add to the JSON:
{
// ... existing translations ...
"studyTimer": {
"title": "Study Timer",
"start": "Start",
"pause": "Pause",
"reset": "Reset"
}
}File: src/i18n/locales/ar.json
{
// ... existing translations ...
"studyTimer": {
"title": "مؤقت الدراسة",
"start": "ابدأ",
"pause": "إيقاف مؤقت",
"reset": "إعادة تعيين"
}
}File: src/pages/Home.jsx
Import and add the timer:
import StudyTimer from '../components/StudyTimer';
// In your JSX, add:
<StudyTimer />- Save all files
- Check the browser (auto-refreshes with Vite!)
- Test the timer functionality
- Switch languages to test translations
- Toggle dark mode to check styling
🎉 Congratulations! You've created your first EduLite component!
Build for reusability and accessibility:
// ❌ BAD: Hardcoded, not reusable
function Button() {
return (
<button className="bg-blue-500 text-white px-4 py-2">
Click Me
</button>
);
}
// ✅ GOOD: Flexible, accessible
function Button({
children,
variant = 'primary',
size = 'medium',
onClick,
disabled = false,
...props
}) {
const variants = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
danger: 'bg-red-500 hover:bg-red-600 text-white'
};
const sizes = {
small: 'px-3 py-1 text-sm',
medium: 'px-4 py-2',
large: 'px-6 py-3 text-lg'
};
return (
<button
className={`
${variants[variant]}
${sizes[size]}
rounded transition-colors duration-200
disabled:opacity-50 disabled:cursor-not-allowed
focus:outline-none focus:ring-2 focus:ring-offset-2
`}
onClick={onClick}
disabled={disabled}
{...props}
>
{children}
</button>
);
}// ❌ BAD: Large image, no optimization
<img src="/huge-image.png" alt="Hero" />
// ✅ GOOD: Optimized loading
import { useState, useEffect } from 'react';
function OptimizedImage({ src, alt, placeholder }) {
const [imageSrc, setImageSrc] = useState(placeholder);
const [loading, setLoading] = useState(true);
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => {
setImageSrc(src);
setLoading(false);
};
}, [src]);
return (
<div className="relative">
<img
src={imageSrc}
alt={alt}
className={`transition-opacity duration-300
${loading ? 'opacity-50' : 'opacity-100'}`}
/>
{loading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="animate-spin h-8 w-8 border-b-2 border-blue-500" />
</div>
)}
</div>
);
}// Lazy load heavy components
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}// ❌ BAD: No accessibility
<div onClick={handleClick}>×</div>
// ✅ GOOD: Accessible
<button
onClick={handleClick}
aria-label="Close dialog"
className="p-2 hover:bg-gray-100 rounded"
>
<span aria-hidden="true">×</span>
</button>function AccessibleMenu() {
const [isOpen, setIsOpen] = useState(false);
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
return (
<div onKeyDown={handleKeyDown}>
<button
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-haspopup="true"
>
Menu
</button>
{isOpen && (
<ul role="menu">
<li role="menuitem" tabIndex="0">Option 1</li>
<li role="menuitem" tabIndex="0">Option 2</li>
</ul>
)}
</div>
);
}// Start with mobile, add desktop styles
function ResponsiveCard({ title, content }) {
return (
<div className="
p-4 bg-white rounded-lg shadow
/* Mobile: Full width */
w-full
/* Tablet: Half width */
md:w-1/2
/* Desktop: Third width */
lg:w-1/3
/* Large desktop: Quarter width */
xl:w-1/4
">
<h3 className="
text-lg font-semibold mb-2
/* Larger text on desktop */
lg:text-xl
">
{title}
</h3>
<p className="text-gray-600">{content}</p>
</div>
);
}function Card({ children }) {
return (
<div className="
/* Light mode */
bg-white text-gray-900
/* Dark mode */
dark:bg-gray-800 dark:text-white
/* Shared styles */
rounded-lg shadow-md p-6
transition-colors duration-200
">
{children}
</div>
);
}// ❌ BAD: Hardcoded text
<h1>Welcome to EduLite</h1>
// ✅ GOOD: Translatable
import { useTranslation } from 'react-i18next';
function Welcome() {
const { t } = useTranslation();
return (
<h1>{t('welcome.title', 'Welcome to EduLite')}</h1>
);
}function Layout({ children }) {
const { i18n } = useTranslation();
const isRTL = i18n.dir() === 'rtl';
return (
<div className={`${isRTL ? 'rtl' : 'ltr'}`} dir={i18n.dir()}>
{children}
</div>
);
}// 1. Create page component
// src/pages/StudyGroups.jsx
import { useTranslation } from 'react-i18next';
function StudyGroups() {
const { t } = useTranslation();
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">
{t('studyGroups.title')}
</h1>
{/* Page content */}
</div>
);
}
export default StudyGroups;
// 2. Add route in App.jsx
import StudyGroups from './pages/StudyGroups';
<Route path="/study-groups" element={<StudyGroups />} />
// 3. Add navigation link
<Link to="/study-groups">{t('nav.studyGroups')}</Link>// src/components/ui/Alert.jsx
import { XMarkIcon } from '@heroicons/react/24/outline';
function Alert({
type = 'info',
title,
message,
onClose,
closable = true
}) {
const types = {
info: 'bg-blue-50 text-blue-800 border-blue-200',
success: 'bg-green-50 text-green-800 border-green-200',
warning: 'bg-yellow-50 text-yellow-800 border-yellow-200',
error: 'bg-red-50 text-red-800 border-red-200'
};
return (
<div className={`${types[type]} border rounded-lg p-4 relative`}>
<div className="flex">
<div className="flex-1">
{title && (
<h3 className="font-semibold mb-1">{title}</h3>
)}
<p>{message}</p>
</div>
{closable && onClose && (
<button
onClick={onClose}
className="ml-4 p-1 hover:bg-black/10 rounded"
aria-label="Close alert"
>
<XMarkIcon className="h-5 w-5" />
</button>
)}
</div>
</div>
);
}
export default Alert;// src/hooks/useApi.js
import { useState, useEffect } from 'react';
function useApi(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data, loading, error } = useApi('/api/users/');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{data.results.map(user => (
<li key={user.id}>{user.username}</li>
))}
</ul>
);
}// Use Context for global state
import { createContext, useContext, useState } from 'react';
const UserContext = createContext();
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="flex flex-col items-center justify-center h-screen">
<h1 className="text-2xl font-bold mb-4">
Oops! Something went wrong.
</h1>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}// Memoize expensive computations
import { useMemo } from 'react';
function ExpensiveList({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// Prevent unnecessary re-renders
import { memo } from 'react';
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
// Component only re-renders if data changes
return <div>{/* Complex rendering */}</div>;
});- Frontend README
- Design System (Coming soon)
- Component questions: Comment in your PR
- Design questions: Discord #design channel
- Quick help: Discord #frontend channel
- Bugs: Create a GitHub issue
**What I'm building:**
A study group card component
**Problem:**
The card looks different in dark mode
**What I tried:**
- Added dark: prefix to Tailwind classes
- Checked parent components for overrides
**Code:**
``jsx
<div className="bg-white dark:bg-gray-800">
{/* Content */}
</div>
``
**Screenshot:**
[Attach screenshot]You now have everything to start building amazing frontend features:
- ✅ Environment set up
- ✅ Architecture understood
- ✅ First component built
- ✅ Standards learned
- ✅ Resources bookmarked
- 1 component = Better user experience for thousands
- 1 optimization = Faster loading on slow networks
- 1 accessibility fix = More inclusive education
- 1 translation = New language community served
Welcome to the frontend team! Let's build interfaces that work for everyone, everywhere. 🎨
💚 Good design is invisible. Great design is accessible. The best design empowers students to focus on learning, not on figuring out the interface.