diff --git a/.gitignore b/.gitignore index 3387bff5..5609c112 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules dist .turbo +.idea \ No newline at end of file diff --git a/.turbo/cookies/1.cookie b/.turbo/cookies/1.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/10.cookie b/.turbo/cookies/10.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/11.cookie b/.turbo/cookies/11.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/12.cookie b/.turbo/cookies/12.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/13.cookie b/.turbo/cookies/13.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/14.cookie b/.turbo/cookies/14.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/15.cookie b/.turbo/cookies/15.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/2.cookie b/.turbo/cookies/2.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/3.cookie b/.turbo/cookies/3.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/4.cookie b/.turbo/cookies/4.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/5.cookie b/.turbo/cookies/5.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/6.cookie b/.turbo/cookies/6.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/7.cookie b/.turbo/cookies/7.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/8.cookie b/.turbo/cookies/8.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/.turbo/cookies/9.cookie b/.turbo/cookies/9.cookie deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/backend/.turbo/turbo-lint.log b/apps/backend/.turbo/turbo-lint.log index ced99303..5ff1db95 100644 --- a/apps/backend/.turbo/turbo-lint.log +++ b/apps/backend/.turbo/turbo-lint.log @@ -1,5 +1,4 @@ - - -> @full-stack-template-typescript-turbo-repo/backend@1.0.0 lint -> eslint . - + +> @full-stack-template-typescript-turbo-repo/backend@1.0.0 lint +> eslint . + diff --git a/apps/backend/package.json b/apps/backend/package.json index 399a65d3..83e8cfe3 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -21,7 +21,7 @@ "@types/cookie-parser": "^1.4.8", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", - "@types/node": "^22.13.5", + "@types/node": "^22.15.3", "eslint": "^9.21.0", "globals": "^15.15.0", "ts-node": "^10.9.2", @@ -29,10 +29,10 @@ "typescript-eslint": "^8.24.1" }, "dependencies": { - "@supabase/supabase-js": "^2.49.1", + "@supabase/supabase-js": "^2.49.4", "cookie-parser": "^1.4.7", "cors": "^2.8.5", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "express": "^4.21.2", "nodemon": "^3.1.9" } diff --git a/apps/backend/server.ts b/apps/backend/server.ts index 0ec8c156..7d686b1b 100644 --- a/apps/backend/server.ts +++ b/apps/backend/server.ts @@ -1,9 +1,18 @@ import cookieParser from 'cookie-parser'; import cors from 'cors'; import dotenv from 'dotenv'; -import express, { Express, NextFunction, Request, Response } from 'express'; +import express, { Express, NextFunction, Request, RequestHandler, Response } from 'express'; +import supabase from './config/supabase.js'; import authRoutes from './routes/authRoutes.js'; +import multer from 'multer'; + + +interface MulterRequest extends Request { + file?: Express.Multer.File; +} +const upload = multer({ limits: { fileSize: 50 * 1024 * 1024 } }); // 50MB limit + dotenv.config(); const app: Express = express(); @@ -21,7 +30,7 @@ interface CorsOptions { const corsOptions: CorsOptions = { origin: (origin, callback) => { - const allowedOrigins = [process.env.FRONTEND_URL || '', process.env.FRONTEND_URL_DEV || '']; + const allowedOrigins = [process.env.FRONTEND_URL || '', process.env.FRONTEND_URL_DEV || '', "https://the-recyclery.vercel.app/"]; if (allowedOrigins.includes(origin || '') || !origin) { callback(null, true); @@ -34,7 +43,16 @@ const corsOptions: CorsOptions = { credentials: true, maxAge: 86400, }; - +app.use(cookieParser()); +app.use( + express.json({ limit: '50mb' }) // ↑ allow big JSON bodies +); +app.use( + express.urlencoded({ // ↑ if you ever post urlencoded too + limit: '50mb', + extended: true, + }) +); app.use(cors(corsOptions)); // if (process.env.NODE_ENV !== 'production') { @@ -44,8 +62,6 @@ app.use(cors(corsOptions)); // }); // } -app.use(cookieParser()); -app.use(express.json()); app.use((req, res, next) => { req.url = req.url.replace(/\/+/g, '/'); @@ -74,6 +90,252 @@ app.use((err: AppError, _req: Request, res: Response, _next: NextFunction) => { }); }); + +app.get('/images/:id', async (req: Request, res: Response) => { + try { + const img_ID = parseInt(req.params.id); + const { data } = await supabase + .from('IMAGES') + .select('bucket_link') + .eq('img_id', img_ID) + .single(); + + res.setHeader('Content-Type', 'application/json'); + res.json({ bucket_link: data?.bucket_link || null }); + } catch (error) { + res.status(500).json({ err: 'Failed to fetch image', error}); + } +}); + + +// GET for getting person in WHO +app.get('/people/:id', (async (req: Request, res: Response) => { + try { + const person_ID = parseInt(req.params.id); + const { data: person, error: dbErr} = await supabase + .from('WHO') + .select('*') + .eq('person_id', person_ID) + .single(); + + if (dbErr) { + return res + .status(500) + .json({ error: 'Failed to update person record' }) + } + if (!person) { + return res + .status(404) + .json({ error: 'Person not found' }) + } + + res.setHeader('Content-Type', 'application/json'); + res.json({ person_name: person?.name || null, person_description: person?.description || null , person_image: person?.person_image || null }); + } catch (error) { + res.status(500).json({ err: 'Failed to fetch person', error }); + } +}) as RequestHandler +); + +// GET for getting hours in HOURS +app.get('/hours/:id', (async (req: Request, res: Response) => { + try { + const hours_ID = parseInt(req.params.id); + const {data: hours, error: dbErr} = await supabase + .from('HOURS') + .select('hours') + .eq('id', hours_ID) + .single(); + + if (dbErr) { + return res + .status(500) + .json({ error: 'Failed to get hours record' }) + } + if (!hours) { + return res + .status(404) + .json({ error: 'Hours not found' }) + } + res.setHeader('Content-Type', 'application/json'); + res.json({ hours_text: hours?.hours || null }); + } catch (error) { + res.status(500).json({ err: 'Failed to fetch hours', error }); + } +}) as RequestHandler +); + +// PUT to upload photos +app.put('/upload/:id', upload.single('file'), (async (req, res) => { + try { + const { id } = req.params; + const file = req.file; + + if (!file) { + return res.status(400).json({ error: 'No file uploaded or missing file name' }); + } + + const fileBuffer = file.buffer; + const fileName = file.originalname; + + const allowedExtensions = ['jpeg', 'png', 'jpg', 'heic', 'gif', 'webp']; + const fileExtension = fileName.split('.').pop()?.toLowerCase(); + + if (!fileExtension || !allowedExtensions.includes(fileExtension)) { + return res.status(400).json({ error: 'Unsupported file type' }); + } + + const uniqueFileName = `images/${id}-${Date.now()}.${fileExtension}`; + + const { error: uploadError } = await supabase.storage + .from('IMAGES') // Replace with your bucket name + .upload(uniqueFileName, fileBuffer, { + contentType: `image/${fileExtension}`, + upsert: true, + }); + + if (uploadError) { + return res.status(500).json({ error: 'Failed to upload image to Supabase' }); + } + + const { data: publicUrlData } = supabase.storage + .from('IMAGES') + .getPublicUrl(uniqueFileName); + + if (!publicUrlData?.publicUrl) { + return res.status(500).json({ error: 'Failed to generate public URL' }); + } + + // Update the database with the new bucket link + const { data: dbdata, error: dbError } = await supabase + .from('IMAGES') + .update({ bucket_link: publicUrlData.publicUrl }) + .eq('img_id', id) + .select('*') + .single(); + + if (dbError) { + return res.status(500).json({ error: 'Failed to update database' }); + } + + if (!dbdata) { + return res.status(404).json({ error: 'Image not found' }); + } + + return res.status(200).json({ + message: 'Image uploaded and database updated successfully', + dbdata, + }); + + } catch (error) { + res.status(500).json({ error }); + } +}) as RequestHandler +); + +// for UploadHours +app.put( + '/uploadhours/:id', + express.json({ limit: '1mb' }), (async (req, res) => { + try { + const { id } = req.params; + const { hours } = req.body as { hours?: string }; + if (!hours) { + return res.status(400).json({ error: 'Missing hours text' }); + } + const { error: dbErr, data: dbData } = await supabase + .from('HOURS') + .update({ hours }) + .eq('id', id) + .select('*') + .single(); + if (dbErr) throw dbErr; + res.json({ message: 'Hours updated', hours: dbData }); + } catch (error) { + res.status(500).json({ error }); + } + }) as RequestHandler +); + +// PUT for upload WHO +app.put('/uploadpeople/:id', upload.single('file'), (async (req: MulterRequest, res: Response) => { + try { + const { id } = req.params; + const { name, description } = req.body as { + name?: string; + description?: string; + }; + + if (!name) { + return res.status(400).json({ error: 'Missing name or description' }) + } + + const updates: Record = { name, description }; + + if (req.file) { + const file = req.file + const ext = file.originalname.split('.').pop()?.toLowerCase() + const allowed = ['jpeg','png','jpg','heic','gif','webp'] + if (!ext || !allowed.includes(ext)) { + return res + .status(400) + .json({ error: 'Unsupported file type' }) + } + + const key = `people/${id}-${Date.now()}.${ext}` + const { error: upErr } = await supabase.storage + .from('who') // your WHO bucket slug + .upload(key, file.buffer, { + contentType: `image/${ext}`, + upsert: true, + }) + if (upErr) { + return res + .status(500) + .json({ error: 'Failed to upload image to WHO bucket' }) + } + // 4) getPublicUrl is sync + const { data: urlData } = supabase.storage + .from('who') + .getPublicUrl(key) + + if (!urlData?.publicUrl) { + return res + .status(500) + .json({ error: 'Failed to generate public URL' }) + } + + updates.person_image = urlData.publicUrl + } + + const { data: person, error: dbErr } = await supabase + .from('WHO') + .update(updates) + .eq('person_id', Number(id)) + .select('*') + .single() + if (dbErr) { + return res + .status(500) + .json({ error: 'Failed to update person record' }) + } + if (!person) { + return res + .status(404) + .json({ error: 'Person not found' }) + } + + return res.status(200).json({ + message: 'Image uploaded and database updated successfully', + person, + }); + + } catch (error) { + res.status(500).json({ error }); + } +}) as RequestHandler +); + const PORT: number = parseInt(process.env.PORT || '3000', 10); app.listen(PORT, () => { // console.log(`Server running on port ${PORT}`); diff --git a/apps/frontend/index.html b/apps/frontend/index.html index e4b78eae..2eaa112b 100644 --- a/apps/frontend/index.html +++ b/apps/frontend/index.html @@ -1,13 +1,12 @@ - - - - - Vite + React + TS - - -
- - + + + + + + +
+ + diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 06a81de5..063836c7 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -12,10 +12,14 @@ "test": "echo \"No tests yet\" && exit 0" }, "dependencies": { + "@tailwindcss/vite": "^4.0.9", + "bootstrap-icons": "^1.11.3", + "lucide-react": "^0.505.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.2.0", - "styled-components": "^6.1.15" + "styled-components": "^6.1.15", + "tailwindcss": "^4.0.9" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/apps/frontend/public/bike.svg b/apps/frontend/public/bike.svg new file mode 100644 index 00000000..5fbf7976 --- /dev/null +++ b/apps/frontend/public/bike.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/public/vite.svg b/apps/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/apps/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/frontend/src/App.css b/apps/frontend/src/App.css index b9d355df..8c0ea4fc 100644 --- a/apps/frontend/src/App.css +++ b/apps/frontend/src/App.css @@ -1,10 +1,3 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - .logo { height: 6em; padding: 1.5em; @@ -33,6 +26,10 @@ } } +html { + scroll-behavior: smooth; +} + .card { padding: 2em; } diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index 5cae1d1b..9d89579c 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -1,16 +1,32 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'; -import { UserProvider } from './contexts/user-provider'; -import { PrivateRoute, PublicOnlyRoute } from './components/protected-routes'; -import NavLayout from './layouts/nav-layouts'; -import Home from './pages/home'; -import Login from './pages/account/login'; -import SignUp from './pages/signup'; -import RequestPasswordReset from './pages/account/request-password-reset'; -import ResetPassword from './pages/account/reset-password'; -import AuthCallback from './pages/account/auth-callback'; -import EmailVerification from './pages/account/email-verifcation'; -import NotFound from './pages/not-found'; +import { PublicOnlyRoute } from './components/protected-routes.tsx'; +import { UserProvider } from './contexts/user-provider.tsx'; +import NavLayout from './layouts/nav-layouts.tsx'; +import Login from './pages/account/login.tsx'; +import Home from './pages/home/home.tsx'; +import SignUp from './pages/signup.tsx'; +// Pages import added + +import WhatWeDo from './pages/about-us/what/what.tsx'; +import WhoWeAre from './pages/about-us/who/who.tsx'; +import Classes from './pages/our-programs/classes/classes.tsx'; +import Freecyclery from './pages/our-programs/freecyclery/freecyclery.tsx'; +import FTWNB from './pages/our-programs/FTWNB/FTWNB.tsx'; +import OpenShop from './pages/our-programs/openshop/openshop.tsx'; +import ContributeFinancially from './pages/support-us/contribute-financially/contribute-financially.tsx'; +import DonateABike from './pages/support-us/donate-a-bike/donate-a-bike.tsx'; +import DonateTime from './pages/support-us/donate-time/donate-time.tsx'; +import OurSupporters from './pages/support-us/our-supporters/our-supporters.tsx'; +import UploadPage from './pages/upload/Upload.tsx'; +import UploadHours from './pages/upload/UploadHours.tsx'; +import UploadPeople from './pages/upload/UploadPeople.tsx'; + import './App.css'; +import AuthCallback from './pages/account/auth-callback.tsx'; +import EmailVerification from './pages/account/email-verifcation.tsx'; +import RequestPasswordReset from './pages/account/request-password-reset.tsx'; +import ResetPassword from './pages/account/reset-password.tsx'; +import NotFound from './pages/not-found.tsx'; function App() { return ( @@ -18,9 +34,8 @@ function App() { }> - }> - } /> - + } /> + }> } /> } /> @@ -29,6 +44,41 @@ function App() { } /> } /> } /> + {/* Protected Route for Upload Page */} + } /> + } /> + } /> + + {/* + Account + */} + {/* + About Us + */} + + } /> + } /> + + {/* + Our Programs + */} + + } /> + } /> + } /> + } /> + + + {/* + Support Us + */} + + } /> + } /> + } /> + } /> + + } /> diff --git a/apps/frontend/src/assets/fonts/Bookman_CE_Bold_Italic.ttf b/apps/frontend/src/assets/fonts/Bookman_CE_Bold_Italic.ttf new file mode 100644 index 00000000..46888366 Binary files /dev/null and b/apps/frontend/src/assets/fonts/Bookman_CE_Bold_Italic.ttf differ diff --git a/apps/frontend/src/assets/fonts/Brandon_Grotesque_Medium.ttf b/apps/frontend/src/assets/fonts/Brandon_Grotesque_Medium.ttf new file mode 100644 index 00000000..c57f7a37 Binary files /dev/null and b/apps/frontend/src/assets/fonts/Brandon_Grotesque_Medium.ttf differ diff --git a/apps/frontend/src/assets/fonts/Roc_Grotesk_Bold.ttf b/apps/frontend/src/assets/fonts/Roc_Grotesk_Bold.ttf new file mode 100644 index 00000000..f6fb1f7b Binary files /dev/null and b/apps/frontend/src/assets/fonts/Roc_Grotesk_Bold.ttf differ diff --git a/apps/frontend/src/assets/images/about-us/what/what-hero.png b/apps/frontend/src/assets/images/about-us/what/what-hero.png new file mode 100644 index 00000000..0a608550 Binary files /dev/null and b/apps/frontend/src/assets/images/about-us/what/what-hero.png differ diff --git a/apps/frontend/src/assets/images/about-us/what/what-section-1.png b/apps/frontend/src/assets/images/about-us/what/what-section-1.png new file mode 100644 index 00000000..e1129c47 Binary files /dev/null and b/apps/frontend/src/assets/images/about-us/what/what-section-1.png differ diff --git a/apps/frontend/src/assets/images/about-us/who/who-hero.png b/apps/frontend/src/assets/images/about-us/who/who-hero.png new file mode 100644 index 00000000..0ef8ab7e Binary files /dev/null and b/apps/frontend/src/assets/images/about-us/who/who-hero.png differ diff --git a/apps/frontend/src/assets/images/home/header-poster.jpg b/apps/frontend/src/assets/images/home/header-poster.jpg new file mode 100644 index 00000000..7ca0cb1e Binary files /dev/null and b/apps/frontend/src/assets/images/home/header-poster.jpg differ diff --git a/apps/frontend/src/assets/images/our-programs/FTWNB/A_Transfeminist-Symbol_black-and-white.png b/apps/frontend/src/assets/images/our-programs/FTWNB/A_Transfeminist-Symbol_black-and-white.png new file mode 100644 index 00000000..22276224 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/FTWNB/A_Transfeminist-Symbol_black-and-white.png differ diff --git a/apps/frontend/src/assets/images/our-programs/FTWNB/FTWN-B.jpg b/apps/frontend/src/assets/images/our-programs/FTWNB/FTWN-B.jpg new file mode 100644 index 00000000..3a6c85de Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/FTWNB/FTWN-B.jpg differ diff --git a/apps/frontend/src/assets/images/our-programs/classes/header-image.png b/apps/frontend/src/assets/images/our-programs/classes/header-image.png new file mode 100644 index 00000000..ebd32309 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/classes/header-image.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/arrow.svg b/apps/frontend/src/assets/images/our-programs/freecyclery/arrow.svg new file mode 100644 index 00000000..16de0238 --- /dev/null +++ b/apps/frontend/src/assets/images/our-programs/freecyclery/arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/circle-sketch.svg b/apps/frontend/src/assets/images/our-programs/freecyclery/circle-sketch.svg new file mode 100644 index 00000000..6d5a4c35 --- /dev/null +++ b/apps/frontend/src/assets/images/our-programs/freecyclery/circle-sketch.svg @@ -0,0 +1,5 @@ + + + diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/delivery-bike.png b/apps/frontend/src/assets/images/our-programs/freecyclery/delivery-bike.png new file mode 100644 index 00000000..7d111ad5 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/delivery-bike.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/earn-a-bike.jpg b/apps/frontend/src/assets/images/our-programs/freecyclery/earn-a-bike.jpg new file mode 100644 index 00000000..11f3ae9f Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/earn-a-bike.jpg differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/freecyclery-header.png b/apps/frontend/src/assets/images/our-programs/freecyclery/freecyclery-header.png new file mode 100644 index 00000000..b7a668ed Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/freecyclery-header.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image.png new file mode 100644 index 00000000..f24a59ca Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image1.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image1.png new file mode 100644 index 00000000..628930b9 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image1.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image10.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image10.png new file mode 100644 index 00000000..ea07fead Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image10.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image11.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image11.png new file mode 100644 index 00000000..32b94c29 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image11.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image12.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image12.png new file mode 100644 index 00000000..0e67adf6 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image12.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image13.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image13.png new file mode 100644 index 00000000..c47d3a52 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image13.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image14.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image14.png new file mode 100644 index 00000000..3706ea3c Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image14.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image15.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image15.png new file mode 100644 index 00000000..fcf41d1a Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image15.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image16.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image16.png new file mode 100644 index 00000000..81ecd1a9 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image16.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image17.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image17.png new file mode 100644 index 00000000..ba90d3d5 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image17.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image2.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image2.png new file mode 100644 index 00000000..2eaf0b17 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image2.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image3.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image3.png new file mode 100644 index 00000000..dba77b6d Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image3.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image4.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image4.png new file mode 100644 index 00000000..91ed1e96 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image4.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image5.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image5.png new file mode 100644 index 00000000..5f18a98f Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image5.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image6.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image6.png new file mode 100644 index 00000000..2f1cf9ad Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image6.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image7.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image7.png new file mode 100644 index 00000000..ae097695 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image7.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image8.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image8.png new file mode 100644 index 00000000..d731a1f4 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image8.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image9.png b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image9.png new file mode 100644 index 00000000..ff90e0f2 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/freecyclery/partners/image9.png differ diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/squiggly-cross.svg b/apps/frontend/src/assets/images/our-programs/freecyclery/squiggly-cross.svg new file mode 100644 index 00000000..2382fffa --- /dev/null +++ b/apps/frontend/src/assets/images/our-programs/freecyclery/squiggly-cross.svg @@ -0,0 +1,7 @@ + + + + diff --git a/apps/frontend/src/assets/images/our-programs/freecyclery/squiggly-line.svg b/apps/frontend/src/assets/images/our-programs/freecyclery/squiggly-line.svg new file mode 100644 index 00000000..ed6f3fe8 --- /dev/null +++ b/apps/frontend/src/assets/images/our-programs/freecyclery/squiggly-line.svg @@ -0,0 +1,5 @@ + + + diff --git a/apps/frontend/src/assets/images/our-programs/openshop/openshop-hero.png b/apps/frontend/src/assets/images/our-programs/openshop/openshop-hero.png new file mode 100644 index 00000000..815106f5 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/openshop/openshop-hero.png differ diff --git a/apps/frontend/src/assets/images/our-programs/openshop/openshop-section-1.png b/apps/frontend/src/assets/images/our-programs/openshop/openshop-section-1.png new file mode 100644 index 00000000..fd9b87c5 Binary files /dev/null and b/apps/frontend/src/assets/images/our-programs/openshop/openshop-section-1.png differ diff --git a/apps/frontend/src/assets/images/support-us/contribute-financially/contribute-hero.png b/apps/frontend/src/assets/images/support-us/contribute-financially/contribute-hero.png new file mode 100644 index 00000000..0c4c181f Binary files /dev/null and b/apps/frontend/src/assets/images/support-us/contribute-financially/contribute-hero.png differ diff --git a/apps/frontend/src/assets/images/support-us/contribute-financially/contribute-image.png b/apps/frontend/src/assets/images/support-us/contribute-financially/contribute-image.png new file mode 100644 index 00000000..c47b5b72 Binary files /dev/null and b/apps/frontend/src/assets/images/support-us/contribute-financially/contribute-image.png differ diff --git a/apps/frontend/src/assets/images/support-us/donate-a-bike/donate-hero.png b/apps/frontend/src/assets/images/support-us/donate-a-bike/donate-hero.png new file mode 100644 index 00000000..023906d1 Binary files /dev/null and b/apps/frontend/src/assets/images/support-us/donate-a-bike/donate-hero.png differ diff --git a/apps/frontend/src/assets/images/support-us/donate-a-bike/donate-image.jpeg b/apps/frontend/src/assets/images/support-us/donate-a-bike/donate-image.jpeg new file mode 100644 index 00000000..4334cf8f Binary files /dev/null and b/apps/frontend/src/assets/images/support-us/donate-a-bike/donate-image.jpeg differ diff --git a/apps/frontend/src/assets/images/support-us/donate-time/donate-time-header.jpg b/apps/frontend/src/assets/images/support-us/donate-time/donate-time-header.jpg new file mode 100644 index 00000000..7a0ee086 Binary files /dev/null and b/apps/frontend/src/assets/images/support-us/donate-time/donate-time-header.jpg differ diff --git a/apps/frontend/src/assets/images/support-us/donate-time/volunteer-fun.jpg b/apps/frontend/src/assets/images/support-us/donate-time/volunteer-fun.jpg new file mode 100644 index 00000000..2893489c Binary files /dev/null and b/apps/frontend/src/assets/images/support-us/donate-time/volunteer-fun.jpg differ diff --git a/apps/frontend/src/assets/images/upload/drop.png b/apps/frontend/src/assets/images/upload/drop.png new file mode 100644 index 00000000..e873edcf Binary files /dev/null and b/apps/frontend/src/assets/images/upload/drop.png differ diff --git a/apps/frontend/src/assets/react.svg b/apps/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/apps/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/frontend/src/components/about-us/who-we-are/member-card.tsx b/apps/frontend/src/components/about-us/who-we-are/member-card.tsx new file mode 100644 index 00000000..2b4a1f99 --- /dev/null +++ b/apps/frontend/src/components/about-us/who-we-are/member-card.tsx @@ -0,0 +1,19 @@ +import { MemberType } from "../../../types.ts"; + +function MemberCard({ name, img, description }: MemberType) { + return ( +
+ {name} +
+ {name} +

{description}

+
+
+ ); +} + +export default MemberCard; diff --git a/apps/frontend/src/components/footer/footer.tsx b/apps/frontend/src/components/footer/footer.tsx new file mode 100644 index 00000000..b89ad5df --- /dev/null +++ b/apps/frontend/src/components/footer/footer.tsx @@ -0,0 +1,81 @@ +import { Bike } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { Button } from '../generic/buttons.tsx'; +import { FacebookIcon, InstagramIcon } from '../icons/icons.tsx'; + +export default function Footer() { + return ( +
+
+
+ +

the recyclery

+
+
+ + +
+
+
+
+
+

Let's stay in touch!

+

+ Have a question? Please feel free to visit us in-person or contact us via phone or + email. +

+
+
+

+ Phone:{' '} + + 773-262-5900 + +

+

+ Email:{' '} + + info@therecyclery.org + +

+

+ Address:{' '} + + 7628 N Paulina St. 60626 Chicago, IL + +

+
+
+
+
+

+ © Copyright 2025{' '} + window.scrollTo(0, 0)}> + The Recyclery Collective + + . All rights reserved. +

+
+ + + + + + +
+
+
+ ); +} diff --git a/apps/frontend/src/components/form/form.tsx b/apps/frontend/src/components/form/form.tsx index 0ad7af5e..4fc490ce 100644 --- a/apps/frontend/src/components/form/form.tsx +++ b/apps/frontend/src/components/form/form.tsx @@ -1,56 +1,4 @@ import { FormEvent, ReactNode } from 'react'; -import { styled } from 'styled-components'; - -const StyledForm = styled.form` - display: flex; - flex-direction: column; - gap: 16px; - padding: 32px; - border-radius: 8px; - background-color: white; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - width: 100%; - max-width: 450px; -`; - -const FormHeader = styled.div` - margin-bottom: 16px; -`; - -const Title = styled.h2` - font-size: 1.8rem; - margin: 0; - text-align: center; -`; - -const Subtitle = styled.p` - color: #666; - margin: 8px 0 0; - text-align: center; -`; - -const SubmitButton = styled.button` - width: 100%; - padding: 12px; - background-color: #646cff; - color: white; - border: none; - border-radius: 4px; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s; - margin-top: 16px; - - &:hover { - background-color: #535bf2; - } - - &:disabled { - background-color: #a5a5a5; - cursor: not-allowed; - } -`; interface FormProps { children: ReactNode; @@ -70,19 +18,26 @@ export function Form({ isSubmitting = false, }: FormProps) { return ( - +
{(title || subtitle) && ( - - {title && {title}} - {subtitle && {subtitle}} - +
+ {title &&

{title}

} + {subtitle &&

{subtitle}

} +
)} {children} - + + ); } diff --git a/apps/frontend/src/components/form/input-password.tsx b/apps/frontend/src/components/form/input-password.tsx index 0489032d..ea7c317d 100644 --- a/apps/frontend/src/components/form/input-password.tsx +++ b/apps/frontend/src/components/form/input-password.tsx @@ -1,14 +1,15 @@ import { ChangeEvent } from 'react'; -import TitledInput from './titled-input'; -import PasswordField from './password-field'; +import PasswordField from './password-field.tsx'; +import TitledInput from './titled-input.tsx'; -interface InputPasswordProps { +export type InputPasswordProps = { title: string; name: string; onChange: (e: ChangeEvent) => void; placeholder?: string; value?: string; required?: boolean; + className?: string; // to allow for tailwind styling (?) } const InputPassword = ({ title, ...rest }: InputPasswordProps) => { diff --git a/apps/frontend/src/components/form/input-text.tsx b/apps/frontend/src/components/form/input-text.tsx index 1324ac81..94261ce0 100644 --- a/apps/frontend/src/components/form/input-text.tsx +++ b/apps/frontend/src/components/form/input-text.tsx @@ -1,8 +1,8 @@ import { ChangeEvent } from 'react'; -import TitledInput from './titled-input'; -import TextField from './text-field'; +import TitledInput from './titled-input.tsx'; +import TextField from './text-field.tsx'; -interface InputTextProps { +export type InputTextProps = { title: string; name: string; onChange: (e: ChangeEvent) => void; diff --git a/apps/frontend/src/components/form/input.tsx b/apps/frontend/src/components/form/input.tsx index dd5902d7..43806a71 100644 --- a/apps/frontend/src/components/form/input.tsx +++ b/apps/frontend/src/components/form/input.tsx @@ -1,5 +1,5 @@ -import InputText from './input-text'; -import InputPassword from './input-password'; +import InputPassword from './input-password.tsx'; +import InputText from './input-text.tsx'; export const Input = { Text: InputText, diff --git a/apps/frontend/src/components/form/password-field.tsx b/apps/frontend/src/components/form/password-field.tsx index d76c5671..b95753eb 100644 --- a/apps/frontend/src/components/form/password-field.tsx +++ b/apps/frontend/src/components/form/password-field.tsx @@ -1,6 +1,5 @@ -import { useState, ChangeEvent } from 'react'; -import { StyledInput, PasswordContainer, IconContainer } from './styles'; -import { EyeIcon, EyeClosedIcon } from './icons'; +import { ChangeEvent, useState } from 'react'; +import { EyeClosedIcon, EyeIcon } from './icons.tsx'; interface PasswordFieldProps { name: string; @@ -17,12 +16,19 @@ const PasswordField = (props: PasswordFieldProps) => { }; return ( - - - +
+ +
{showPassword ? : } - - +
+
); }; diff --git a/apps/frontend/src/components/form/text-field.tsx b/apps/frontend/src/components/form/text-field.tsx index 04367b43..df1a2f90 100644 --- a/apps/frontend/src/components/form/text-field.tsx +++ b/apps/frontend/src/components/form/text-field.tsx @@ -1,5 +1,4 @@ import { ChangeEvent } from 'react'; -import { StyledInput } from './styles'; interface TextFieldProps { name: string; @@ -10,7 +9,14 @@ interface TextFieldProps { } const TextField = (props: TextFieldProps) => { - return ; + return ( + + ); }; export default TextField; diff --git a/apps/frontend/src/components/form/titled-input.tsx b/apps/frontend/src/components/form/titled-input.tsx index 33c38baa..15f84d94 100644 --- a/apps/frontend/src/components/form/titled-input.tsx +++ b/apps/frontend/src/components/form/titled-input.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { InputContainer, InputName, InputTitle, RedSpan } from './styles'; interface TitledInputProps { title: string; @@ -9,13 +8,14 @@ interface TitledInputProps { const TitledInput = ({ title, required, children }: TitledInputProps) => { return ( - - - {title} - {required && *} - + // this div had the absolute tag which was causing problems, make sure it's relatives so other styling isn't thrown out the window +
+

+ {title} + {required && *} +

{children} - +
); }; diff --git a/apps/frontend/src/components/generic/accordion.tsx b/apps/frontend/src/components/generic/accordion.tsx new file mode 100644 index 00000000..230f2153 --- /dev/null +++ b/apps/frontend/src/components/generic/accordion.tsx @@ -0,0 +1,45 @@ +import { CircleCheckBig, CircleMinus, CirclePlus } from 'lucide-react'; +import { useState } from 'react'; +import { AccordionItem } from '../../types.tsx'; +import DashedBorder from './dashed-border.tsx'; + +interface AccordionProps { + items: AccordionItem[]; +} + +export default function Accordion({ items }: AccordionProps) { + const [toggled, setToggled] = useState(0); + + return ( +
+ {items.map((item, index) => ( + + +
+

{item.content}

+
+
+ ))} +
+ ); +} diff --git a/apps/frontend/src/components/generic/bg-image.tsx b/apps/frontend/src/components/generic/bg-image.tsx new file mode 100644 index 00000000..08d5d1a1 --- /dev/null +++ b/apps/frontend/src/components/generic/bg-image.tsx @@ -0,0 +1,22 @@ +interface BgImageProps { + children: React.ReactNode; + image: string | undefined; + opacity?: number; + className?: string; +} + +export function BgImage({ children, image, opacity = 0.4, className = '' }: BgImageProps) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/frontend/src/components/generic/buttons.tsx b/apps/frontend/src/components/generic/buttons.tsx new file mode 100644 index 00000000..6a4a6f1d --- /dev/null +++ b/apps/frontend/src/components/generic/buttons.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +interface ButtonProps { + children: React.ReactNode; + onClick?: () => void; + className?: string; + color?: 'blue' | 'orange' | 'white'; +} + +export function Button({ children, onClick, className, color = 'blue' }: ButtonProps) { + return ( + + ); +} diff --git a/apps/frontend/src/components/generic/dashed-border.tsx b/apps/frontend/src/components/generic/dashed-border.tsx new file mode 100644 index 00000000..ae5de679 --- /dev/null +++ b/apps/frontend/src/components/generic/dashed-border.tsx @@ -0,0 +1,27 @@ +interface DashedBorderProps { + children: React.ReactNode; + className?: string; + color?: string; + dashSize?: number; + dashGap?: number; +} + +export default function DashedBorder({ + children, + className, + color = '7FA03E', + dashSize = 6, + dashGap = 14, +}: DashedBorderProps) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/frontend/src/components/generic/edit-hours-button.tsx b/apps/frontend/src/components/generic/edit-hours-button.tsx new file mode 100644 index 00000000..4fa366bc --- /dev/null +++ b/apps/frontend/src/components/generic/edit-hours-button.tsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom'; +import { useUser } from '../../hooks/useUser.tsx'; + +interface EditLinkProps { + id: string; + className?: string; +} + +export function EditLink({ id, className = ''}: EditLinkProps) { + const { isAuthenticated } = useUser(); + if (!isAuthenticated) { + return null; + } + return ( + + + + ) +} diff --git a/apps/frontend/src/components/generic/edit-image-button.tsx b/apps/frontend/src/components/generic/edit-image-button.tsx new file mode 100644 index 00000000..5c0b844d --- /dev/null +++ b/apps/frontend/src/components/generic/edit-image-button.tsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom'; +import { useUser } from '../../hooks/useUser.tsx'; + +interface EditLinkProps { + id: string; + className?: string; +} + +export function EditLink({ id, className = ''}: EditLinkProps) { + const { isAuthenticated } = useUser(); + if (!isAuthenticated) { + return null; + } + return ( + + + + ) +} diff --git a/apps/frontend/src/components/generic/edit-people-button.tsx b/apps/frontend/src/components/generic/edit-people-button.tsx new file mode 100644 index 00000000..f81c1211 --- /dev/null +++ b/apps/frontend/src/components/generic/edit-people-button.tsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom'; +import { useUser } from '../../hooks/useUser.tsx'; + +interface EditLinkProps { + id: string; + className?: string; +} + +export function EditPeople({ id, className = ''}: EditLinkProps) { + const { isAuthenticated } = useUser(); + if (!isAuthenticated) { + return null; + } + return ( + + + + ) +} diff --git a/apps/frontend/src/components/generic/styled-tags.tsx b/apps/frontend/src/components/generic/styled-tags.tsx new file mode 100644 index 00000000..97070b66 --- /dev/null +++ b/apps/frontend/src/components/generic/styled-tags.tsx @@ -0,0 +1,63 @@ +import React, { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; + +export type HeadingProps = { + children: ReactNode; + className?: string; +}; + +export function H1({ children, className }: HeadingProps) { + return

{children}

; +} + +export function H2({ children, className }: HeadingProps) { + return

{children}

; +} + +export function H3({ children, className }: HeadingProps) { + return

{children}

; +} + +interface AProps { + to: string; + children: React.ReactNode; + className?: string; +} + +export function A({ to, children, className }: AProps) { + const onClick = () => { + if (to.startsWith('/')) { + window.scrollTo(0, 0); + } + }; + + return ( + + {children} + + ); +} + +interface SectionProps { + children: React.ReactNode; + className?: string; + id?: string; + style?: React.CSSProperties; + tan?: boolean; +} + +export function Section({ children, className, id, style, tan = false }: SectionProps) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/frontend/src/components/home/calendar-section.tsx b/apps/frontend/src/components/home/calendar-section.tsx new file mode 100644 index 00000000..766e5d04 --- /dev/null +++ b/apps/frontend/src/components/home/calendar-section.tsx @@ -0,0 +1,27 @@ +import { H2, Section } from '../generic/styled-tags.tsx'; + +function CalendarSection() { + return ( +
+

our event calendar

+

+ The calendar below shows all events for the Recyclery. Any upcoming events will also be + updated here! +

+ + +
+ ); +} + +export default CalendarSection; diff --git a/apps/frontend/src/components/home/hero-section.tsx b/apps/frontend/src/components/home/hero-section.tsx new file mode 100644 index 00000000..ea9667e8 --- /dev/null +++ b/apps/frontend/src/components/home/hero-section.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import headerPoster from '../../assets/images/home/header-poster.jpg'; +import { BgImage } from '../generic/bg-image.tsx'; +import { Button } from '../generic/buttons.tsx'; +import { H1 } from '../generic/styled-tags.tsx'; + +function HeroSection() { + const [imageURL, setImageURL] = useState(undefined); + + useEffect(() => { + fetch(`/images/1`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setImageURL(data.bucket_link); + } else { + setImageURL(headerPoster); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setImageURL(headerPoster); + }); + }, []); + + return ( + +

the recyclery

+

+ The Recyclery Collective is an educational bike shop that promotes sustainability by giving + access to tools, skills, and opportunities for collaboration. +

+
+ + +
+
+ ); +} + +export default HeroSection; diff --git a/apps/frontend/src/components/home/program.tsx b/apps/frontend/src/components/home/program.tsx new file mode 100644 index 00000000..cb060089 --- /dev/null +++ b/apps/frontend/src/components/home/program.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; +import { Button } from '../generic/buttons.tsx'; +import { H3 } from '../generic/styled-tags.tsx'; + +interface ProgramProps { + children: ReactNode; + title: string; + learnMoreLink: string; +} + +export default function Program({ children, title, learnMoreLink }: ProgramProps) { + return ( +
+

{title}

+

{children}

+ +
+ ); +} diff --git a/apps/frontend/src/components/home/programs-section.tsx b/apps/frontend/src/components/home/programs-section.tsx new file mode 100644 index 00000000..e4c7c3fa --- /dev/null +++ b/apps/frontend/src/components/home/programs-section.tsx @@ -0,0 +1,59 @@ +import Program from './program.tsx'; +import { A, H2, Section } from '../generic/styled-tags.tsx'; + +export default function ProgramsSection() { + return ( +
+

our programs

+
+ + You can work on your own bike during Open Shop. All levels of experience are welcome. +
+
+ For femme, trans, women and non-binary community,{' '} + FTWN-B Shop Time (Sundays 3-6pm) is another opportunity to + work on your bike. +
+
+ HOURS: +
+ Tuesdays 5-7pm +
+ Thursdays 7-9pm +
+ Saturdays 11am-2pm +
+ + Volunteers fix bikes for donation, manage bike sales, assist with Open Shop, and help run + the shop. All levels of mechanical experience are welcome, and we encourage you to use + your prior skills and life experience to help power our mission & vision. +
+
+ Volunteers must first fill out an application and attend an orientation. +
+
+ HOURS: +
+ Mondays 12-3pm, and 5:30-7:30pm +
+ Wednesdays 6-8pm +
+ Thursday 10am-1pm +
+ + We offer multiple classes, including our introductory Tune Up Class, and our more advanced + Overhaul Class. The classes are meant for adults, and you should bring your own bike. +
+
+ To register, visit our{' '} + online shop. +
+
+ HOURS: +
+ Variable, see sign up page +
+
+
+ ); +} diff --git a/apps/frontend/src/components/home/video-section.tsx b/apps/frontend/src/components/home/video-section.tsx new file mode 100644 index 00000000..125e3b6e --- /dev/null +++ b/apps/frontend/src/components/home/video-section.tsx @@ -0,0 +1,18 @@ +import { H2, Section } from '../generic/styled-tags.tsx'; + +export default function VideoSection() { + return ( +
+

introductory video

+ +
+ ); +} diff --git a/apps/frontend/src/components/icons/icons.tsx b/apps/frontend/src/components/icons/icons.tsx new file mode 100644 index 00000000..373cf4c3 --- /dev/null +++ b/apps/frontend/src/components/icons/icons.tsx @@ -0,0 +1,27 @@ +export function FacebookIcon(props: React.SVGProps) { + return ( + + + + ); +} + +export function InstagramIcon(props: React.SVGProps) { + return ( + + + + ); +} diff --git a/apps/frontend/src/components/navigation/nav-bar-item.tsx b/apps/frontend/src/components/navigation/nav-bar-item.tsx new file mode 100644 index 00000000..a68abaf1 --- /dev/null +++ b/apps/frontend/src/components/navigation/nav-bar-item.tsx @@ -0,0 +1,57 @@ +import { ChevronDown } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { NavContentType } from '../../types.ts'; + +export default function NavbarItem({ title, items }: NavContentType) { + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const itemRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (itemRef.current && !itemRef.current.contains(event.target as Node)) { + setIsFlyoutOpen(false); + } + } + + document.addEventListener('click', handleClickOutside, true); + + return () => { + document.removeEventListener('click', handleClickOutside, true); + }; + }, [itemRef, setIsFlyoutOpen]); + + return ( +
+ +
+ {items.map(item => { + return ( + + ); + })} +
+
+ ); +} diff --git a/apps/frontend/src/components/navigation/nav-bar.tsx b/apps/frontend/src/components/navigation/nav-bar.tsx index 2e5758b9..f9792432 100644 --- a/apps/frontend/src/components/navigation/nav-bar.tsx +++ b/apps/frontend/src/components/navigation/nav-bar.tsx @@ -1,96 +1,55 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { styled } from 'styled-components'; -import { useUser } from '../../hooks/useUser'; -import LogoutModal from './logout-modal'; - -const StyledNav = styled.nav` - display: flex; - gap: 10px; - padding: 10px 20px; - font-size: 20px; -`; - -const LeftAligned = styled.div` - flex: 1; - display: flex; - gap: 10px; -`; - -const LogoPlaceholder = styled.button` - padding: 0; - font-size: 1.7rem; - font-weight: bold; - font-family: monospace; - background: none; - border: none; - cursor: pointer; -`; - -const Button = styled.button` - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; - font-size: 16px; -`; - -const PrimaryButton = styled(Button)` - background-color: #646cff; - color: white; - border: 1px solid #646cff; - - &:hover { - background-color: #535bf2; - } -`; - -const SecondaryButton = styled(Button)` - background-color: transparent; - border: 1px solid #646cff; - color: #646cff; - - &:hover { - background-color: rgba(100, 108, 255, 0.1); - } -`; +import { Bike, Menu } from 'lucide-react'; +import { ReactNode, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { NavContent } from '../../content/nav-content.tsx'; +import { Button } from '../generic/buttons.tsx'; +import NavbarItem from './nav-bar-item.tsx'; +import SideMenu from './side-menu.tsx'; + +export type NavbarSubMenuItemType = { + icon: ReactNode; + title: string; + description: string; + destination: string; +}; export default function NavBar() { - const [isModalOpen, setIsModalOpen] = useState(false); - const navigate = useNavigate(); - const { user, logout } = useUser(); - - const handleLogoutClick = () => { - setIsModalOpen(true); - }; - - const handleModalClose = () => { - setIsModalOpen(false); - }; - - const handleLogoutConfirm = async () => { - try { - await logout(); - setIsModalOpen(false); - navigate('/', { replace: true }); - } catch (error) { - console.error('Logout error:', error); - } - }; + const [isSideMenuOpen, setIsSideMenuOpen] = useState(false); return ( - - - navigate('/')}>[LOGO] - - {user ? ( - Log Out - ) : ( - <> - navigate('/signup')}>Sign Up - navigate('/login')}>Login - - )} - - + <> + + + ); } diff --git a/apps/frontend/src/components/navigation/side-menu-item.tsx b/apps/frontend/src/components/navigation/side-menu-item.tsx new file mode 100644 index 00000000..92f737b3 --- /dev/null +++ b/apps/frontend/src/components/navigation/side-menu-item.tsx @@ -0,0 +1,55 @@ +import { ChevronDown } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { NavContentType } from '../../types.tsx'; + +type SideMenuItemProps = { + currentActiveAccordion: string; + handleAccordionSelect: (arg0: string) => void; + isSideMenuOpen: boolean; + setIsSideMenuOpen: (arg0: boolean) => void; +} & NavContentType; + +export default function SideMenuItem({ + currentActiveAccordion, + handleAccordionSelect, + isSideMenuOpen, + setIsSideMenuOpen, + title, + items, +}: SideMenuItemProps) { + return ( +
+ +
+ {items.map(item => { + return ( + + ); + })} +
+
+ ); +} diff --git a/apps/frontend/src/components/navigation/side-menu.tsx b/apps/frontend/src/components/navigation/side-menu.tsx new file mode 100644 index 00000000..9c2cd5cd --- /dev/null +++ b/apps/frontend/src/components/navigation/side-menu.tsx @@ -0,0 +1,83 @@ +import { Bike, XIcon } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { NavContent } from '../../content/nav-content.tsx'; +import { Button } from '../generic/buttons.tsx'; +import SideMenuItem from './side-menu-item.tsx'; + +interface SideMenuProps { + isSideMenuOpen: boolean; + setIsSideMenuOpen: (arg0: boolean) => void; +} + +export default function SideMenu({ isSideMenuOpen, setIsSideMenuOpen }: SideMenuProps) { + const menuRef = useRef(null); + const [currentActiveAccordion, setCurrentActiveAccordion] = useState(''); + + function handleAccordionSelect(accordionTitle: string) { + if (currentActiveAccordion === accordionTitle) { + setCurrentActiveAccordion(''); + } else { + setCurrentActiveAccordion(accordionTitle); + } + } + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsSideMenuOpen(false); + } + } + + document.addEventListener('click', handleClickOutside, true); + + return () => { + document.removeEventListener('click', handleClickOutside, true); + }; + }, [menuRef, setIsSideMenuOpen]); + + return ( +
+
+
+
+ + the recyclery +
+ +
+
+
+ {NavContent.map((item, index) => ( + + ))} +
+
+
+ + +
+
+
+ ); +} diff --git a/apps/frontend/src/components/our-programs/classes/class-card.tsx b/apps/frontend/src/components/our-programs/classes/class-card.tsx new file mode 100644 index 00000000..09a6c79a --- /dev/null +++ b/apps/frontend/src/components/our-programs/classes/class-card.tsx @@ -0,0 +1,57 @@ +import { Link } from 'react-router-dom'; +import { Button } from '../../generic/buttons.tsx'; + +interface ClassCardProps { + title: string; + level: 'beginner' | 'advanced'; + sessions: string; + description: string; + buttonText: string; + buttonLink: string; +} + +export default function ClassCard({ + title, + level, + sessions, + description, + buttonText, + buttonLink, +}: ClassCardProps) { + const levelStyles = { + beginner: 'bg-green-500 text-white', + advanced: 'text-white', + }; + + return ( +
+

+ {title} +

+ + {level} + +

{sessions}

+

{description}

+ +
+ ); +} diff --git a/apps/frontend/src/components/our-programs/classes/class-description.tsx b/apps/frontend/src/components/our-programs/classes/class-description.tsx new file mode 100644 index 00000000..d3ef35d6 --- /dev/null +++ b/apps/frontend/src/components/our-programs/classes/class-description.tsx @@ -0,0 +1,34 @@ +import { H2, Section } from '../../generic/styled-tags.tsx'; + +export default function ClassDescription() { + return ( +
+
+

+ If you have never fixed a flat tire before, we recommend taking the Tune Up Class first. +

+

+ We can provide a bike that will be donated locally after the overhaul is complete, but we + also encourage students to bring their own! +

+
+
+

mechanics 101

+

+ Learn the basics with our seasoned mechanics for a two-session bicycle maintenance course. + Give your own geared bike a tune-up and become educated on proper terminology for + componentry and tools, the basics of fixing flat tires, chain replacement, and brake & + shifting adjustments. We keep classes small, so grab your limited spot while you can! +

+
+
+

overhaul class

+

+ Over six sessions, seasoned Recyclery mechanic instructor(s) will walk you through how to + disassemble, reassemble, and adjust all major mechanical systems of your bicycle in + addition to answering any questions you have along the way! +

+
+
+ ); +} diff --git a/apps/frontend/src/components/our-programs/classes/class-hero.tsx b/apps/frontend/src/components/our-programs/classes/class-hero.tsx new file mode 100644 index 00000000..ec5ef4f3 --- /dev/null +++ b/apps/frontend/src/components/our-programs/classes/class-hero.tsx @@ -0,0 +1,14 @@ +import { BgImage } from '../../generic/bg-image.tsx'; +import { H1 } from '../../generic/styled-tags.tsx'; + +export default function ClassHero({ heroimageURL }: { heroimageURL?: string }) { + return ( + +

classes

+

+ We teach bicycle maintenance through our introductory Tune Up Class and our advanced + Overhaul Class. +

+
+ ); +} diff --git a/apps/frontend/src/components/our-programs/classes/class-signup.tsx b/apps/frontend/src/components/our-programs/classes/class-signup.tsx new file mode 100644 index 00000000..e67efd69 --- /dev/null +++ b/apps/frontend/src/components/our-programs/classes/class-signup.tsx @@ -0,0 +1,28 @@ +import { H2, Section } from '../../generic/styled-tags.tsx'; +import ClassCard from './class-card.tsx'; + +export default function ClassSignup() { + return ( +
+

sign up

+
+ + +
+
+ ); +} diff --git a/apps/frontend/src/components/our-programs/freecyclery/about-section.tsx b/apps/frontend/src/components/our-programs/freecyclery/about-section.tsx new file mode 100644 index 00000000..40502c7d --- /dev/null +++ b/apps/frontend/src/components/our-programs/freecyclery/about-section.tsx @@ -0,0 +1,28 @@ +import { H2, Section } from '../../generic/styled-tags.tsx'; +import squigglyLine from '../../../assets/images/our-programs/freecyclery/squiggly-line.svg'; + +export default function AboutSection({ imageURL }: { imageURL?: string }) { + return ( +
+
+

about our program

+

+ We work with local social service agencies to connect people in need with a free bike that + was serviced by volunteer mechanics and checked over by a Recyclery mechanic. +

+
+
+ squiggly line + woman holding a bike +
+
+ ); +} diff --git a/apps/frontend/src/components/our-programs/freecyclery/earn-a-bike.tsx b/apps/frontend/src/components/our-programs/freecyclery/earn-a-bike.tsx new file mode 100644 index 00000000..08388bea --- /dev/null +++ b/apps/frontend/src/components/our-programs/freecyclery/earn-a-bike.tsx @@ -0,0 +1,50 @@ +import { Link } from 'react-router-dom'; +import { Button } from '../../generic/buttons.tsx'; +import DashedBorder from '../../generic/dashed-border.tsx'; +import { H2, H3, Section } from '../../generic/styled-tags.tsx'; + +export default function EarnABike() { + return ( +
+

earn-a-bike fellowship programs

+

+ As an alternative to a referral from a partner organization, we offer opportunities for + adults and youth to earn a Freecyclery bike through our Earn-a-Bike Fellowship programs. +

+

+ Designated times where fellows are invited to volunteer, program requires 12 hours and $50 + tuition for the bike, helmet, and lock. +

+
+
+ +
+

1.

+

Complete the Volunteer Application

+
+ +
+
+
+ +
+

2.

+

Complete the Online or In-Person Orientation

+
+ +
+
+
+
+ ); +} diff --git a/apps/frontend/src/components/our-programs/freecyclery/how-it-works.tsx b/apps/frontend/src/components/our-programs/freecyclery/how-it-works.tsx new file mode 100644 index 00000000..2a3f98d6 --- /dev/null +++ b/apps/frontend/src/components/our-programs/freecyclery/how-it-works.tsx @@ -0,0 +1,66 @@ +import { Bike, Megaphone } from 'lucide-react'; +import arrow from '../../../assets/images/our-programs/freecyclery/arrow.svg'; +import circleSketch from '../../../assets/images/our-programs/freecyclery/circle-sketch.svg'; +import { H2, H3, Section } from '../../generic/styled-tags.tsx'; + +export default function HowItWorks() { + return ( +
+

how does it work?

+

+ The Freecyclery Program provides + + TWO + + ways for people to get bikes for free: +

+
+
+
+ + arrow +
+

Referrals

+

+ Our dedicated{' '} + + Freecyclery Partners + {' '} + refer individuals to us to receive a free bicycle. +

+
+
+
+ + arrow +
+

Earn-A-Bike Fellowship

+

+ Adults and youth can earn a Freecyclery bike through our{' '} + + Earn-a-Bike Fellowship + {' '} + programs. +

+
+
+
+ ); +} diff --git a/apps/frontend/src/components/our-programs/freecyclery/make-referral.tsx b/apps/frontend/src/components/our-programs/freecyclery/make-referral.tsx new file mode 100644 index 00000000..f2dbbb10 --- /dev/null +++ b/apps/frontend/src/components/our-programs/freecyclery/make-referral.tsx @@ -0,0 +1,50 @@ +import { Mail } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import deliveryBike from '../../../assets/images/our-programs/freecyclery/delivery-bike.png'; +import squigglyCross from '../../../assets/images/our-programs/freecyclery/squiggly-cross.svg'; +import { referralAccordionContent } from '../../../content/referral-info.ts'; +import Accordion from '../../generic/accordion.tsx'; +import DashedBorder from '../../generic/dashed-border.tsx'; +import { A, H2, Section } from '../../generic/styled-tags.tsx'; + +export default function MakeReferral() { + return ( +
+

make a referral today

+
+ +
+

contact us

+

Send an email with the following information:

+ + + info@therecyclery.org + + delivery bike +
+ +
+
+

+ Please contact us if you need more information about the referral process. If you are + interested in becoming a referral partner, please review this{' '} + + standard letter to potential referral partners + {' '} + or contact us! We are always open to exploring new collaborations. +

+
+ ); +} diff --git a/apps/frontend/src/components/our-programs/freecyclery/partners.tsx b/apps/frontend/src/components/our-programs/freecyclery/partners.tsx new file mode 100644 index 00000000..71f7f105 --- /dev/null +++ b/apps/frontend/src/components/our-programs/freecyclery/partners.tsx @@ -0,0 +1,25 @@ +import { H2, Section } from '../../generic/styled-tags.tsx'; + +export default function Partners() { + const allImages = import.meta.glob( + '../../../assets/images/our-programs/freecyclery/partners/*.png' + ); + const images = Object.keys(allImages).map(path => { + const imagePath = path.replace('../../../', '/src/'); + return { + src: imagePath, + alt: path.split('/').pop()?.split('.')[0] || '', + }; + }); + + return ( +
+

freecyclery partners

+
+ {images.map(image => ( + {image.alt} + ))} +
+
+ ); +} diff --git a/apps/frontend/src/components/protected-routes.tsx b/apps/frontend/src/components/protected-routes.tsx index ab6c777e..5a940b7b 100644 --- a/apps/frontend/src/components/protected-routes.tsx +++ b/apps/frontend/src/components/protected-routes.tsx @@ -1,5 +1,5 @@ import { Navigate, Outlet } from 'react-router-dom'; -import { useUser } from '../hooks/useUser'; +import { useUser } from '../hooks/useUser.tsx'; export function PrivateRoute() { const { user, isLoading } = useUser(); diff --git a/apps/frontend/src/components/support-us/donate-time/hero-section.tsx b/apps/frontend/src/components/support-us/donate-time/hero-section.tsx new file mode 100644 index 00000000..07611ac2 --- /dev/null +++ b/apps/frontend/src/components/support-us/donate-time/hero-section.tsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom'; +import { BgImage } from '../../generic/bg-image.tsx'; +import { Button } from '../../generic/buttons.tsx'; +import { A, H1 } from '../../generic/styled-tags.tsx'; + +function HeroSection({ image }: { image?: string }) { + return ( + +

become a volunteer

+ +

+ Already have access? Sign in{' '} + + here + +

+
+ ); +} + +export default HeroSection; diff --git a/apps/frontend/src/components/support-us/donate-time/role-card.tsx b/apps/frontend/src/components/support-us/donate-time/role-card.tsx new file mode 100644 index 00000000..a998e454 --- /dev/null +++ b/apps/frontend/src/components/support-us/donate-time/role-card.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { H3 } from '../../generic/styled-tags.tsx'; + +export type RoleCardProps = { + name: string; + icon: React.ReactNode; + description: string; +}; + +function RoleCard({ name, icon, description }: RoleCardProps) { + return ( +
+ {icon} +

{name}

+

{description}

+
+ ); +} + +export default RoleCard; diff --git a/apps/frontend/src/components/support-us/donate-time/roles.tsx b/apps/frontend/src/components/support-us/donate-time/roles.tsx new file mode 100644 index 00000000..28c34fac --- /dev/null +++ b/apps/frontend/src/components/support-us/donate-time/roles.tsx @@ -0,0 +1,69 @@ +import { BrushCleaning, Calendar, Camera, HeartHandshake, ReceiptText, ThumbsUp, Wrench } from 'lucide-react'; +import { H2, Section } from '../../generic/styled-tags.tsx'; +import RoleCard, { RoleCardProps } from './role-card.tsx'; + +const roles: RoleCardProps[] = [ + { + name: 'Open Shop Greeter', + icon: , + description: + 'Welcome in our community members, give them the run-down on our Community Guidelines, and get them set up with a host and stand.', + }, + { + name: 'Freecyclery Mechanic', + icon: , + description: + 'Fix up bikes for people in need. No experience required, we will have volunteer hosts who will share their skills with you. Great way to become more self sufficient in working on bikes, and help out your community in the process.', + }, + { + name: 'Social Media Advisor', + icon: , + description: + 'We are always looking to expand our social media presence and spread our mission.', + }, + { + name: 'Bike Sale Assistants', + icon: , + description: + 'Great for people who have a wealth of knowledge about bikes, and who have an interest in getting more people riding bikes. This is a great way to support the recyclery as our sale bikes are our nonprofit’s major source of income.', + }, + { + name: 'Neatness and Tidiness Team', + icon: , + description: + 'We have quarterly deep cleanings, and we could always use more people to put away the tiny bits and pieces that end up everywhere but where they actually belong!', + }, + { + name: 'Events and Outreach Volunteers', + icon: , + description: + 'Great for people who are free on the weekends, or who are interested in special events. We love spreading our mission at community events, having plenty of extra hands at our events, and always looking for new events to attend and include in our newsletter.', + }, + { + name: 'Photographer', + icon: , + description: + 'A picture is worth a thousand words! We love showing people what we’re doing via social media, newsletters, and our website.', + }, +]; + +function Roles() { + return ( +
+
+

+ + our volunteer roles + +

+
+ {roles.map(role => { + return ; + })} +
+
+
+ ); +} + +export default Roles; diff --git a/apps/frontend/src/components/support-us/donate-time/volunteer-mission.tsx b/apps/frontend/src/components/support-us/donate-time/volunteer-mission.tsx new file mode 100644 index 00000000..3bb17b35 --- /dev/null +++ b/apps/frontend/src/components/support-us/donate-time/volunteer-mission.tsx @@ -0,0 +1,44 @@ +import { H2, Section } from '../../generic/styled-tags.tsx'; +import { Button } from '../../generic/buttons.tsx'; +import { Link } from 'react-router-dom'; + +function VolunteerMission({ image }: { image?: string }) { + return ( +
+
+

volunteers and our mission

+

+ Recyclery volunteers are the people power that allow our bicycle related programs to move + forward. As Freecyclery Mechanics, they work on discarded bicycles to turn them into + sustainable transportation for those in need. Our most experienced volunteers and staff + take responsibility in sharing their skills and expertise with our learning volunteers + during Volunteer Hours. +

+

+ Our Open Shop program is also run by volunteers, helping community members work on their + own bicycles. We are always looking for friendly volunteers to welcome our patrons into + Open Shop. We have committees of volunteers to make well informed decisions about our + marketing, resource development, technical needs, and events & outreach. The collaboration + of our volunteers with one another and the community is what makes a lasting difference + for our larger community. We depend on a diverse set of volunteer skills and interests to + move our programs ahead and allow them to thrive. +

+

+ We are thankful to our volunteers who transform lives by embodying our mission. Their hard + work and dedication will make a difference in our neighborhood and the lives of those we + serve and support. +

+ +
+ a volunteer helping someone fix a bike +
+ ); +} + +export default VolunteerMission; diff --git a/apps/frontend/src/components/upload/upload-link.tsx b/apps/frontend/src/components/upload/upload-link.tsx new file mode 100644 index 00000000..ac2af441 --- /dev/null +++ b/apps/frontend/src/components/upload/upload-link.tsx @@ -0,0 +1,17 @@ +import { Link } from 'react-router-dom'; + +export default function ImageGallery({ images }: { images: { id: string; url: string }[] }) { + return ( +
+ {images.map((image) => ( + + {`Image + + ))} +
+ ); +} \ No newline at end of file diff --git a/apps/frontend/src/components/users/users-list.tsx b/apps/frontend/src/components/users/users-list.tsx index e8390cdb..15e998d9 100644 --- a/apps/frontend/src/components/users/users-list.tsx +++ b/apps/frontend/src/components/users/users-list.tsx @@ -1,51 +1,5 @@ import { useEffect, useState } from 'react'; -import { styled } from 'styled-components'; -import type { User } from '../../../types/auth'; - -const UsersContainer = styled.div` - margin-top: 2rem; - width: 100%; - max-width: 800px; - margin-left: auto; - margin-right: auto; -`; - -const UserCard = styled.div` - padding: 1rem; - margin-bottom: 1rem; - border: 1px solid #e2e8f0; - border-radius: 0.5rem; - background-color: white; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -`; - -const UserInfo = styled.div` - display: flex; - gap: 1rem; - align-items: center; -`; - -const UserName = styled.h3` - margin: 0; - font-size: 1.1rem; - font-weight: 600; -`; - -const UserEmail = styled.p` - margin: 0; - color: #666; -`; - -const ErrorMessage = styled.div` - color: #e53e3e; - padding: 1rem; - text-align: center; -`; - -const LoadingMessage = styled.div` - text-align: center; - padding: 1rem; -`; +import type { User } from '../../../types/auth.ts'; export default function UsersList() { const [users, setUsers] = useState([]); @@ -82,31 +36,31 @@ export default function UsersList() { }, []); if (loading) { - return Loading users...; + return
Loading users...
; } if (error) { - return Error: {error}; + return
Error: {error}
; } return ( - +
{users.length === 0 ? (

No users found.

) : ( users.map(user => ( - - +
+
- +

{user.firstname} {user.lastname} ({user.username || 'No username'}) - - {user.email} +

+

{user.email}

- - +
+
)) )} - +
); } diff --git a/apps/frontend/src/content/collection-points.ts b/apps/frontend/src/content/collection-points.ts new file mode 100644 index 00000000..64e69c1d --- /dev/null +++ b/apps/frontend/src/content/collection-points.ts @@ -0,0 +1,40 @@ +import { CollectionPointType } from '../types.ts'; + +export const collectionPoints: CollectionPointType[] = [ + { + title: 'Cog Cycles Chicago', + url: 'http://www.cogcycleschicago.com/', + address: '3217 W Bryn Mawr Ave, Chicago, IL 60659', + phone: '312-373-0095', + }, + { + title: 'Samcycle Electric Bikes', + url: 'https://samcycle.online/', + address: '144 W Northwest Hwy, Palatine, IL 60067', + phone: '847-485-7014', + }, + { + title: 'Green Machine Cycles', + url: 'https://www.greenmachinecycles.com/', + address: '1634 W Montrose Ave, Chicago, IL 60613', + phone: '773-506-2453<', + }, + { + title: 'BFF Bikes', + url: 'https://www.bffbikes.com/', + address: '2056 N Damen Ave, Chicago, IL 60647', + phone: '773-666-5153', + }, + { + title: 'Big City Bikes', + url: 'https://www.bigcitybikes.org/', + address: '2425 N Ashland Ave, Chicago, IL 60614', + phone: '773-906-5311', + }, + { + title: 'Patagonia Fulton Market', + url: 'https://dropoffyouroldbike.splashthat.com/', + address: '1115 W Fulton Market, Chicago, IL 60607', + phone: '312-951-0518', + }, +]; diff --git a/apps/frontend/src/content/donation-info.ts b/apps/frontend/src/content/donation-info.ts new file mode 100644 index 00000000..1d4eefd6 --- /dev/null +++ b/apps/frontend/src/content/donation-info.ts @@ -0,0 +1,20 @@ +import { AccordionItem } from '../types.ts'; + +export const donationAccordionContent: AccordionItem[] = [ + { + title: '$50', + content: 'Pays for two people in need to get suited with helmets and locks.', + }, + { + title: '$200', + content: 'Gives a refurbished bike to someone in need.', + }, + { + title: '$500', + content: 'Provides job training for seven young adults.', + }, + { + title: '$800', + content: 'Provides new tools for our community and volunteers.', + }, +]; diff --git a/apps/frontend/src/content/members.ts b/apps/frontend/src/content/members.ts new file mode 100644 index 00000000..e0b4fb13 --- /dev/null +++ b/apps/frontend/src/content/members.ts @@ -0,0 +1,39 @@ +import { MemberType } from '@/types.ts'; + +export const members: MemberType[] = [ + { + name: 'Charlie', + img: '', + description: 'Daytime Freecyclery Host', + }, + { + name: 'Max', + img: '', + description: 'Daytime Freecyclery Host', + }, + { + name: 'Dana', + img: '', + description: 'Evening Freecyclery Host', + }, + { + name: 'Tom', + img: '', + description: 'Collective Member', + }, + { + name: 'Tzip', + img: '', + description: 'Freecyclery Coordinator & Collective Member', + }, + { + name: 'Rohan', + img: '', + description: 'Collective Member', + }, + { + name: 'Nina', + img: '', + description: 'Collective Member', + }, +]; diff --git a/apps/frontend/src/content/nav-content.tsx b/apps/frontend/src/content/nav-content.tsx new file mode 100644 index 00000000..10bad6c0 --- /dev/null +++ b/apps/frontend/src/content/nav-content.tsx @@ -0,0 +1,150 @@ +import { Bike } from 'lucide-react'; +import { NavContentType } from '../types.ts'; + +export const NavContent: NavContentType[] = [ + { + title: 'About Us', + items: [ + { + icon: , + title: 'What We Do', + description: 'Read about our history, mission, and values', + destination: '/about-us/what', + }, + { + icon: , + title: 'Who We Are', + description: 'Get to know some of our team members', + destination: '/about-us/who', + }, + ], + }, + { + title: 'Our Programs', + items: [ + { + icon: , + title: 'Open Shop', + description: + 'Use our tools to fix your own bike with the instruction of Recyclery mechanics', + destination: '/our-programs/openshop', + }, + { + icon: , + title: 'Freecyclery', + description: 'Earn a free bike through our referral and fellowship programs', + destination: '/our-programs/freecyclery', + }, + { + icon: , + title: 'FTWN-B', + description: 'Explore a safe space for under-represented members of the bike community', + destination: '/our-programs/ftwnb', + }, + { + icon: , + title: 'Classes', + description: 'Learn about the process of maintaining and overhauling your own bike', + destination: '/our-programs/classes', + }, + ], + }, + { + title: 'Support Us', + items: [ + { + icon: , + title: 'Contribute Financially', + description: 'Donate money directly to help fund our programs', + destination: '/support-us/contribute-financially', + }, + { + icon: , + title: 'Our Supporters', + description: 'Learn about our top supporters', + destination: '/support-us/our-supporters', + }, + { + icon: , + title: 'Donate a Bike', + description: 'Donate your old bikes to either support our programs or help people in need', + destination: '/support-us/donate-a-bike', + }, + { + icon: , + title: 'Become a volunteer', + description: 'Help improve our programs by offering your skills and expertise', + destination: '/support-us/donate-time', + }, + ], + }, +]; + +// export const AboutUsSubItems: NavbarSubMenuItemType[] = [ +// { +// icon: , +// title: 'What We Do', +// description: 'Read about our history, mission, and values', +// destination: '/about-us/what', +// }, +// { +// icon: , +// title: 'Who We Are', +// description: 'Get to know some of our team members', +// destination: '/about-us/who', +// }, +// ]; + +// export const OurProgramsSubItems: NavbarSubMenuItemType[] = [ +// { +// icon: , +// title: 'Open Shop', +// description: 'Use our tools to fix your own bike with the instruction of Recyclery mechanics', +// destination: '/our-programs/openshop', +// }, +// { +// icon: , +// title: 'Freecyclery', +// description: 'Earn a free bike through our referral and fellowship programs', +// destination: '/our-programs/freecyclery', +// }, +// { +// icon: , +// title: 'FTWN-B', +// description: 'Explore a safe space for under-represented members of the bike community', +// destination: '/our-programs/ftwnb', +// }, +// { +// icon: , +// title: 'Classes', +// description: 'Learn about the process of maintaining and overhauling your own bike', +// destination: '/our-programs/classes', +// }, +// ]; + +// export const SupportUsSubItems: NavbarSubMenuItemType[] = [ +// { +// icon: , +// title: 'Support Us', +// description: "Donate money to help fund The Recyclery's programs", +// destination: '/support-us/contribute-financially', +// }, +// { +// icon: , +// title: 'Our Supporters', +// description: 'Learn about our top supporters', +// destination: '/support-us/our-supporters', +// }, +// { +// icon: , +// title: 'Donate a Bike', +// description: 'Donate your old bikes to either support our programs or help people in need', +// destination: '/support-us/donate-a-bike', +// }, +// { +// icon: , +// title: 'Become a volunteer', +// description: 'Help improve our programs by offering your skills and expertise', +// destination: '/support-us/donate-time', +// }, +// ]; diff --git a/apps/frontend/src/content/referral-info.ts b/apps/frontend/src/content/referral-info.ts new file mode 100644 index 00000000..2517cb3d --- /dev/null +++ b/apps/frontend/src/content/referral-info.ts @@ -0,0 +1,23 @@ +import { AccordionItem } from '../types.ts'; + +export const referralAccordionContent: AccordionItem[] = [ + { + title: 'Client Info', + content: 'What are the client’s first and last names, pronouns, height, weight and age?', + }, + { + title: 'Client’s Preferred Bike Type', + content: + 'Does the client have a preferred bike type? We will do our best to match a client with a bike of their choice, but are constrained by what we have available at any given time.', + }, + { + title: 'Why Does this Client Need a Bike?', + content: + 'Please help us understand how having a working bike will make a difference to this client.', + }, + { + title: 'The $25 Helmet & Lock Cost', + content: + 'Will the agency or the client pay the $25 to cover the lock and helmet cost? Our shop depends on donations from individuals and organizations, so your help to defray our costs is appreciated!', + }, +]; diff --git a/apps/frontend/src/contexts/user-context.tsx b/apps/frontend/src/contexts/user-context.tsx index e0b7c7c2..f1cab886 100644 --- a/apps/frontend/src/contexts/user-context.tsx +++ b/apps/frontend/src/contexts/user-context.tsx @@ -1,5 +1,5 @@ import { createContext } from 'react'; -import type { UserContextType } from '../../types/auth'; +import type { UserContextType } from '../../types/auth.ts'; export const UserContext = createContext({ user: null, diff --git a/apps/frontend/src/contexts/user-provider.tsx b/apps/frontend/src/contexts/user-provider.tsx index 54bd070d..abc105ac 100644 --- a/apps/frontend/src/contexts/user-provider.tsx +++ b/apps/frontend/src/contexts/user-provider.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, ReactNode } from 'react'; import React from 'react'; -import type { User, UserContextType } from '../../types/auth'; -import { UserContext } from './user-context'; +import type { User, UserContextType } from '../../types/auth.ts'; +import { UserContext } from './user-context.tsx'; interface UserProviderProps { children: ReactNode; diff --git a/apps/frontend/src/hooks/useUser.tsx b/apps/frontend/src/hooks/useUser.tsx index 676099eb..0f61644f 100644 --- a/apps/frontend/src/hooks/useUser.tsx +++ b/apps/frontend/src/hooks/useUser.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; -import { UserContext } from '../contexts/user-context'; -import type { UserContextType } from '../../types/auth'; +import type { UserContextType } from '../../types/auth.ts'; +import { UserContext } from '../contexts/user-context.tsx'; export const useUser = (): UserContextType => { const context = useContext(UserContext); diff --git a/apps/frontend/src/index.css b/apps/frontend/src/index.css index 0f13b738..2de7a650 100644 --- a/apps/frontend/src/index.css +++ b/apps/frontend/src/index.css @@ -1,10 +1,95 @@ +@import "tailwindcss"; + +@font-face { + font-family: "bookman"; + font-display: swap; + src: url("../src/assets/fonts/Bookman_CE_Bold_Italic.ttf"); +} + +@font-face { + font-family: "brandon"; + font-display: swap; + src: url("../src/assets/fonts/Brandon_Grotesque_Medium.ttf"); +} + +@font-face { + font-family: "roc"; + font-display: swap; + src: url("../src/assets/fonts/Roc_Grotesk_Bold.ttf"); +} + +@theme { + --color-maroon-500: oklch(31.47% 0.1034 21.93); + --color-maroon-600: oklch(29.47% 0.1034 21.93); + --color-maroon-700: oklch(27.47% 0.1034 21.93); + --color-maroon-800: oklch(25.47% 0.1034 21.93); + --color-maroon-900: oklch(23.47% 0.1034 21.93); + + --color-tan-500: oklch(89.82% 0.04 67.28); + --color-tan-600: oklch(84.82% 0.04 67.28); + --color-tan-700: oklch(79.82% 0.04 67.28); + --color-tan-800: oklch(74.82% 0.04 67.28); + --color-tan-900: oklch(69.82% 0.04 67.28); + + --color-darkblue-500: oklch(24.55% 0.0188 229.7); + --color-darkblue-600: oklch(22.55% 0.0188 229.7); + --color-darkblue-700: oklch(20.55% 0.0188 229.7); + --color-darkblue-800: oklch(18.55% 0.0188 229.7); + --color-darkblue-900: oklch(16.55% 0.0188 229.7); + + --color-orange-500: oklch(46.47% 0.1183 41.76); + --color-orange-600: oklch(43.47% 0.1183 41.76); + --color-orange-700: oklch(40.47% 0.1183 41.76); + --color-orange-800: oklch(37.47% 0.1183 41.76); + --color-orange-900: oklch(34.47% 0.1183 41.76); + + --color-blue-500: oklch(59.03% 0.0733 232.9); + --color-blue-600: oklch(54.03% 0.0733 232.9); + --color-blue-700: oklch(49.03% 0.0733 232.9); + --color-blue-800: oklch(44.03% 0.0733 232.9); + --color-blue-900: oklch(39.03% 0.0733 232.9); + + --color-green-500: oklch(65.96% 0.1425 124.05); + --color-green-600: oklch(61.96% 0.1425 124.05); + --color-green-700: oklch(57.96% 0.1425 124.05); + --color-green-800: oklch(53.96% 0.1425 124.05); + --color-green-900: oklch(50.96% 0.1425 124.05); + + --color-background: oklch(98.56% 0.0084 56.32); + + --text-heading1: 32px; + --text-heading2: 28px; + --text-subheading1: 24px; + --text-subheading2: 22px; + --text-body1: 20px; + --text-body2: 18px; + + --font-bookman: bookman, sans-serif; + --font-brandon: brandon, sans-serif; + --font-roc: roc, sans-serif; + + --breakpoint-xs: 350px; +} + :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; + font-family: var(--font-roc), Avenir, Helvetica, Arial, sans-serif; + font-size: var(--text-body2); + + background-color: var(--color-background); font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + +/* Hide scrollbar in Chrome, Safari, and Opera */ +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar in IE, Edge, and Firefox */ +.no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} diff --git a/apps/frontend/src/layouts/nav-layouts.tsx b/apps/frontend/src/layouts/nav-layouts.tsx index 2c7f1c08..18ba52ac 100644 --- a/apps/frontend/src/layouts/nav-layouts.tsx +++ b/apps/frontend/src/layouts/nav-layouts.tsx @@ -1,18 +1,13 @@ import { Outlet } from 'react-router-dom'; -import { styled } from 'styled-components'; -import NavBar from '../components/navigation/nav-bar'; - -const Layout = styled.div` - height: 100vh; - display: flex; - flex-direction: column; -`; +import Footer from '../components/footer/footer.tsx'; +import NavBar from '../components/navigation/nav-bar.tsx'; export default function NavLayout() { return ( - +
- +
+
); } diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx index df655eae..ea9e3630 100644 --- a/apps/frontend/src/main.tsx +++ b/apps/frontend/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import './index.css'; import App from './App.tsx'; +import './index.css'; createRoot(document.getElementById('root')!).render( diff --git a/apps/frontend/src/pages/about-us/what/what.tsx b/apps/frontend/src/pages/about-us/what/what.tsx new file mode 100644 index 00000000..055ad3ea --- /dev/null +++ b/apps/frontend/src/pages/about-us/what/what.tsx @@ -0,0 +1,166 @@ +import { useEffect, useState } from 'react'; +import WhatHero from '../../../assets/images/about-us/what/what-hero.png'; +import WhatSection1 from '../../../assets/images/about-us/what/what-section-1.png'; +import { BgImage } from '../../../components/generic/bg-image.tsx'; +import { H1, H2, H3, Section } from '../../../components/generic/styled-tags.tsx'; + +function WhatWeDo() { + const [heroimageURL, setHeroImageURL] = useState(undefined); + const [whatsec1URL, setWhatSec1ImageURL] = useState(undefined); + + useEffect(() => { + fetch(`/images/2`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setHeroImageURL(data.bucket_link); + } else { + setHeroImageURL(WhatHero); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setHeroImageURL(WhatHero); + }); + }, []); + + useEffect(() => { + fetch(`/images/3`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setWhatSec1ImageURL(data.bucket_link); + } else { + setWhatSec1ImageURL(WhatSection1); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setWhatSec1ImageURL(WhatSection1); + }); + }, []); + + return ( +
+ What We Do - The Recyclery + +

what we do

+

+ The seed idea of The Recyclery was planted in 2005, with 2025 marking our 20th anniversary + as well as our milestone of refurbishing 10,000 bikes. +

+
+
+
+
+

our mission

+

+ The Recyclery Collective is an educational bike shop that promotes sustainability by + giving access to tools, skills, and opportunities for collaboration. +

+
+
+

our vision

+

+ We envision a diverse, resilient neighborhood filled with knowledgeable, self-reliant + cyclists. +

+
+
+ Person with bike +
+
+

our core values

+
+
+
+

collaboration

+
+

+ We create opportunities for people of all backgrounds to work, share, and learn + together to support a practical form of transportation. +

+

+ We don't just share benches and tools, but we share our time and expertise with + one another. +

+
+
+
+

social justice

+
+

+ We provide tools and resources to empower underserved populations and nurture + community leaders. +

+
+
+
+
+
+

ecological sustainability

+
+

+ We encourage the mindful use of resources and promote cycling as a part of + building more resilient communities. +

+

+ We source all of our bicycles and componentry from donations, saving them from + being landfilled or deteriorating without use. By giving these bikes and parts a + second chance at life, we are helping people transform their transportation + habits. +

+
+
+
+

celebrating everything we do

+
+

+ We like to have fun while working hard to make bicycles and mechanics accessible. +

+

+ Stay updated on what we're celebrating by joining our newsletter. +

+
+
+
+
+
+
+

testimonials

+
+
+

+ “Lack of transportation is a major barrier in both treatment and community integration + of individuals with psychiatric disabilities. For many, their bike is their most + valuable possession, serving as reliable transportation to work, school, or necessary + medical appointments.” +

+
+

Brent Peterson

+

Director of Development Thresholds

+
+
+
+
+

+ “I work but did not have enough money to pay for my rent, groceries, and other items I + need plus bus fare. Having a bike made it easier to make my money last and not worry + about calling off work.” +

+
+

Client

+

Connections for the Homeless

+
+
+
+
+
+ ); +} + +export default WhatWeDo; diff --git a/apps/frontend/src/pages/about-us/who/who.tsx b/apps/frontend/src/pages/about-us/who/who.tsx new file mode 100644 index 00000000..d3ecbf98 --- /dev/null +++ b/apps/frontend/src/pages/about-us/who/who.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react'; +import WhoHero from '../../../assets/images/about-us/who/who-hero.png'; +import MemberCard from '../../../components/about-us/who-we-are/member-card.tsx'; +import { BgImage } from '../../../components/generic/bg-image.tsx'; +import { H1, H2, Section } from '../../../components/generic/styled-tags.tsx'; +import { members } from '../../../content/members.ts'; + +function WhoWeAre() { + const [imageURL, setImageURL] = useState(undefined); + + useEffect(() => { + fetch(`/images/4`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setImageURL(data.bucket_link); + } else { + setImageURL(WhoHero); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setImageURL(WhoHero); + }); + }, []); + + return ( +
+ Who We Are - The Recyclery + +

who we are

+

+ Get to know some of our team members. +

+
+
+

staff and collective members

+
+ {members.map((member, index) => { + return ; + })} +
+

+ We are collective owned and operated. The Recyclery is organized by a board of volunteer + "collective members." All collective members contribute three hours per week and commit to + attending the majority of Collective Meetings which are held on the 2nd and 4th Monday of + each month. The meetings are open to anyone who has a stake in The Recyclery. Collective + Members make decisions through a consensus process.{' '} +

+
+
+ ); +} + +export default WhoWeAre; diff --git a/apps/frontend/src/pages/account/auth-callback.tsx b/apps/frontend/src/pages/account/auth-callback.tsx index 29d2ac55..4640fd8e 100644 --- a/apps/frontend/src/pages/account/auth-callback.tsx +++ b/apps/frontend/src/pages/account/auth-callback.tsx @@ -1,19 +1,6 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { styled } from 'styled-components'; -import { useUser } from '../../hooks/useUser'; - -const Container = styled.div` - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; -`; - -const LoadingText = styled.p` - font-size: 1rem; - color: #333; -`; +import { useUser } from '../../hooks/useUser.tsx'; export default function AuthCallback() { const navigate = useNavigate(); @@ -71,8 +58,9 @@ export default function AuthCallback() { }, [navigate, checkAuth]); return ( - - Completing authentication... - +
+ Completing Authentication - The Recyclery +

Completing authentication...

+
); } diff --git a/apps/frontend/src/pages/account/email-verifcation.tsx b/apps/frontend/src/pages/account/email-verifcation.tsx index 43e30ce0..4530d9fa 100644 --- a/apps/frontend/src/pages/account/email-verifcation.tsx +++ b/apps/frontend/src/pages/account/email-verifcation.tsx @@ -1,27 +1,5 @@ import { useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { styled } from 'styled-components'; - -const VerificationPage = styled.div` - flex: 1 0 0; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - padding-top: 100px; -`; - -const Title = styled.h1` - font-size: 2.5em; - font-weight: bold; - margin: 0; -`; - -const Subtitle = styled.h2` - font-size: 1.5em; - margin: 0; - font-weight: normal; -`; type VerificationStatus = 'checking' | 'success' | 'error'; @@ -43,20 +21,27 @@ export default function EmailVerification() { }, [searchParams, navigate]); return ( - - {status === 'checking' && Checking verification status...} +
+ Email Verification - The Recyclery + {status === 'checking' && ( +

Checking verification status...

+ )} {status === 'success' && ( <> - Email Verified! - {"You'll be redirected to login in 3 seconds..."} +

Email Verified!

+

+ {"You'll be redirected to login in 3 seconds..."} +

)} {status === 'error' && ( <> - Verification Failed - Please try signing up again or contact support. +

Verification Failed

+

+ Please try signing up again or contact support. +

)} - +
); } diff --git a/apps/frontend/src/pages/account/login.tsx b/apps/frontend/src/pages/account/login.tsx index 6549bd0f..abd540e6 100644 --- a/apps/frontend/src/pages/account/login.tsx +++ b/apps/frontend/src/pages/account/login.tsx @@ -1,83 +1,7 @@ -import { useState, FormEvent, ChangeEvent } from 'react'; -import { Link, useNavigate, useLocation } from 'react-router-dom'; -import { styled } from 'styled-components'; -import { useUser } from '../../hooks/useUser'; -import { Input } from '../../components/form/input'; -import { RedSpan } from '../../components/form/styles'; - -const LoginContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 80vh; -`; - -const LoginForm = styled.form` - display: flex; - flex-direction: column; - gap: 16px; - width: 100%; - max-width: 400px; - padding: 32px; - border-radius: 8px; - background-color: white; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -`; - -const FormTitle = styled.h2` - margin: 0 0 16px 0; - text-align: center; - font-size: 1.8rem; -`; - -const ButtonContainer = styled.div` - margin-top: 8px; -`; - -const LoginButton = styled.button` - width: 100%; - padding: 10px; - border: none; - border-radius: 4px; - background-color: #646cff; - color: white; - font-size: 1rem; - cursor: pointer; - transition: background-color 0.2s; - - &:hover { - background-color: #535bf2; - } - - &:disabled { - background-color: #a5a5a5; - cursor: not-allowed; - } -`; - -const StyledLink = styled(Link)` - color: #646cff; - text-decoration: none; - font-size: 0.9rem; - margin-top: 8px; - align-self: flex-end; - - &:hover { - text-decoration: underline; - } -`; - -const SignupPrompt = styled.div` - margin-top: 16px; - text-align: center; -`; - -const ErrorMessage = styled(RedSpan)` - text-align: center; - display: block; - margin-bottom: 8px; -`; +import { ChangeEvent, FormEvent, useState } from 'react'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { Input } from '../../components/form/input.tsx'; +import { useUser } from '../../hooks/useUser.tsx'; interface LoginState { email: string; @@ -131,9 +55,13 @@ export default function Login() { }; return ( - - - Log In +
+ Log In - The Recyclery +
+

Log In

{locationState?.message && (
@@ -141,7 +69,7 @@ export default function Login() {
)} - {error && {error}} + {error && {error}} - Forgot Password? + + Forgot Password? + - - +
+ +
+ +
+ Don't have an account?{' '} + + Sign up + +
+ +
); } diff --git a/apps/frontend/src/pages/account/request-password-reset.tsx b/apps/frontend/src/pages/account/request-password-reset.tsx index c4807b9b..6a0c2ce0 100644 --- a/apps/frontend/src/pages/account/request-password-reset.tsx +++ b/apps/frontend/src/pages/account/request-password-reset.tsx @@ -1,77 +1,6 @@ -import { useState, FormEvent, ChangeEvent } from 'react'; +import { ChangeEvent, FormEvent, useState } from 'react'; import { Link } from 'react-router-dom'; -import { styled } from 'styled-components'; -import { useUser } from '../../hooks/useUser'; - -const Container = styled.div` - max-width: 400px; - margin: 40px auto; - padding: 20px; -`; - -const Form = styled.form` - display: flex; - flex-direction: column; - gap: 20px; -`; - -const Input = styled.input` - padding: 10px; - border: 1px solid #ccc; - border-radius: 4px; -`; - -const Button = styled.button` - padding: 10px; - background-color: #646cff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.2s ease; - - &:hover { - background-color: #535bf2; - } - - &:disabled { - background-color: #ccc; - cursor: not-allowed; - } -`; - -const ErrorMessage = styled.div` - color: red; - margin-top: 10px; - padding: 10px; - border-radius: 4px; - background-color: #ffe6e6; -`; - -const SuccessMessage = styled.div` - color: #2e7d32; - margin-top: 10px; - padding: 10px; - border-radius: 4px; - background-color: #edf7ed; -`; - -const StyledLink = styled(Link)` - color: #646cff; - text-decoration: none; - text-align: center; - margin-top: 10px; - - &:hover { - text-decoration: underline; - } -`; - -const Title = styled.h2` - text-align: center; - color: #333; - margin-bottom: 20px; -`; +import { useUser } from '../../hooks/useUser.tsx'; export default function RequestPasswordReset() { const [email, setEmail] = useState(''); @@ -96,11 +25,13 @@ export default function RequestPasswordReset() { }; return ( - - Reset Password +
+ Reset Password - The Recyclery +

Reset Password

{!success ? ( -
- + - - {error && {error}} - Back to Login -
+ + {error && ( +
+ {error} +
+ )} + + Back to Login + + ) : (
- +
Password reset instructions have been sent to your email. Please check your inbox and follow the instructions to reset your password. If you don't receive the email within a few minutes, please check your spam folder. - - Back to Login +
+ + Back to Login +
)} - +
); } diff --git a/apps/frontend/src/pages/account/reset-password.tsx b/apps/frontend/src/pages/account/reset-password.tsx index 69ad1f41..42e6f549 100644 --- a/apps/frontend/src/pages/account/reset-password.tsx +++ b/apps/frontend/src/pages/account/reset-password.tsx @@ -1,86 +1,6 @@ -import { useEffect, useState, FormEvent, ChangeEvent } from 'react'; +import { ChangeEvent, FormEvent, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { styled } from 'styled-components'; -import { useUser } from '../../hooks/useUser'; - -const Container = styled.div` - max-width: 400px; - margin: 40px auto; - padding: 20px; -`; - -const Form = styled.form` - display: flex; - flex-direction: column; - gap: 20px; -`; - -const Input = styled.input` - padding: 10px; - border: 1px solid #ccc; - border-radius: 4px; -`; - -const Button = styled.button` - padding: 10px; - background-color: #646cff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - - &:disabled { - background-color: #ccc; - cursor: not-allowed; - } -`; - -const ErrorMessage = styled.div` - color: red; - margin-top: 10px; -`; - -const PasswordStrength = styled.div<{ strength: 'weak' | 'medium' | 'strong' | 'none' }>` - height: 5px; - margin-top: 5px; - background-color: ${({ strength }) => { - switch (strength) { - case 'weak': - return '#ff4d4d'; - case 'medium': - return '#ffd700'; - case 'strong': - return '#32cd32'; - default: - return '#ccc'; - } - }}; - width: ${({ strength }) => { - switch (strength) { - case 'weak': - return '33%'; - case 'medium': - return '66%'; - case 'strong': - return '100%'; - default: - return '0%'; - } - }}; - transition: all 0.3s ease; -`; - -const PasswordRequirements = styled.ul` - font-size: 0.8rem; - color: #666; - margin-top: 5px; - padding-left: 20px; -`; - -const PasswordContainer = styled.div` - display: flex; - flex-direction: column; -`; +import { useUser } from '../../hooks/useUser.tsx'; export default function ResetPassword() { const [password, setPassword] = useState(''); @@ -108,26 +28,6 @@ export default function ResetPassword() { } }, []); - const checkPasswordStrength = (pass: string): 'weak' | 'medium' | 'strong' | 'none' => { - if (!pass) return 'none'; - - const hasUpperCase = /[A-Z]/.test(pass); - const hasLowerCase = /[a-z]/.test(pass); - const hasNumbers = /\d/.test(pass); - const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(pass); - - const strength = - (hasUpperCase ? 1 : 0) + - (hasLowerCase ? 1 : 0) + - (hasNumbers ? 1 : 0) + - (hasSpecialChar ? 1 : 0) + - (pass.length >= 8 ? 1 : 0); - - if (strength >= 4) return 'strong'; - if (strength >= 2) return 'medium'; - return 'weak'; - }; - const validatePassword = (pass: string): string[] => { const requirements: string[] = []; @@ -194,38 +94,45 @@ export default function ResetPassword() { }; return ( - +
+ Set New Password - The Recyclery

Set New Password

-
- - +
+ ) => setPassword(e.target.value)} required /> - +
{password && ( - +
    {validatePassword(password).map((req, index) => (
  • {req}
  • ))} - +
)} - - + ) => setConfirmPassword(e.target.value)} required /> - - {error && {error}} - - + + {error &&
{error}
} + +
); } diff --git a/apps/frontend/src/pages/home.tsx b/apps/frontend/src/pages/home.tsx deleted file mode 100644 index a5370a08..00000000 --- a/apps/frontend/src/pages/home.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { styled } from 'styled-components'; -import { useUser } from '../hooks/useUser'; -import UsersList from '../components/users/users-list'; - -const TextContainer = styled.div` - display: flex; - flex-direction: column; - gap: 20px; -`; - -const HomePage = styled.div` - flex: 1 0 0; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - padding: 2rem; -`; - -const Title = styled.h1` - font-size: 2.5em; - font-weight: bold; - margin: 0; -`; - -const Subtitle = styled.h2` - font-size: 1.5em; - margin: 0; - font-weight: normal; -`; - -export default function Home() { - const { user } = useUser(); - - return ( - - - Home Page - Welcome, {user?.firstname || 'User'}! - - - - ); -} diff --git a/apps/frontend/src/pages/home/home.tsx b/apps/frontend/src/pages/home/home.tsx new file mode 100644 index 00000000..32195794 --- /dev/null +++ b/apps/frontend/src/pages/home/home.tsx @@ -0,0 +1,16 @@ +import CalendarSection from '../../components/home/calendar-section.tsx'; +import HeroSection from '../../components/home/hero-section.tsx'; +import ProgramsSection from '../../components/home/programs-section.tsx'; +import VideoSection from '../../components/home/video-section.tsx'; + +export default function Home() { + return ( +
+ The Recyclery + + + + +
+ ); +} diff --git a/apps/frontend/src/pages/not-found.tsx b/apps/frontend/src/pages/not-found.tsx index ed123994..a8283c8c 100644 --- a/apps/frontend/src/pages/not-found.tsx +++ b/apps/frontend/src/pages/not-found.tsx @@ -1,29 +1,11 @@ -import { styled } from 'styled-components'; - -const NotFoundContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 60vh; - text-align: center; -`; - -const Title = styled.h1` - font-size: 3rem; - margin-bottom: 1rem; -`; - -const Subtitle = styled.p` - font-size: 1.2rem; - color: #666; -`; - export default function NotFound() { return ( - - 404 - Page Not Found - Sorry, the page you are looking for does not exist. - +
+ 404 - Page Not Found - The Recyclery +

404 - Page Not Found

+

+ Sorry, the page you are looking for does not exist. +

+
); } diff --git a/apps/frontend/src/pages/our-programs/FTWNB/FTWNB.tsx b/apps/frontend/src/pages/our-programs/FTWNB/FTWNB.tsx new file mode 100644 index 00000000..425c86e1 --- /dev/null +++ b/apps/frontend/src/pages/our-programs/FTWNB/FTWNB.tsx @@ -0,0 +1,87 @@ +import { useEffect, useState } from 'react'; +import bikerepairPic from '../../../assets/images/our-programs/FTWNB/FTWN-B.jpg'; +import { A, H1, H2, Section } from '../../../components/generic/styled-tags.tsx'; + +function FTWNB() { + const [imageURL, setImageURL] = useState(undefined); + + useEffect(() => { + fetch(`/images/9`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setImageURL(data.bucket_link); + } else { + setImageURL(bikerepairPic); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setImageURL(bikerepairPic); + }); + }, []); + + return ( + <> + FTWNB - The Recyclery +
+ {/* for future migrate to H2 tag once color prop is made */} +

FTWN-B Shop

+

+ for people identifying as femme, trans, women, or non-binary. +

+ + {/* rounded button */} +
+

sundays 3-6 PM

+
+
+ + {/* Row one of text and pic */} +
+ {/* Left text block */} +
+

in this program...

+
    +
  • Bring in your bike and learn some repair skills from talented mechanics
  • + +
  • + Volunteer and fix up bikes for our{' '} + Freecyclery Program +
  • + +
  • Hang out with fellow FTWN-B bike riders
  • +
+
+ + {/* image */} +
+ Bike repair +
+
+ +
+

+ This program is specifically catering to the Femme, Trans, Women, and Non-Binary members + of our community.* We're aiming to provide a welcoming, safe space for individuals who + have historically been excluded by the bicycling community. +

+ +

+ This program is an amalgamation of our programs. You can come to learn, socialize, + volunteer, work on your bike, and more! Our host Skylar would love to help you work on + your own bike or teach you skills on how to work on second hand bikes. +

+
+ +
+

+ *We ask for people who don't identify as FTWN-B to come to our general{' '} + Open Shop Hours instead +

+
+ + ); +} + +export default FTWNB; diff --git a/apps/frontend/src/pages/our-programs/classes/classes.tsx b/apps/frontend/src/pages/our-programs/classes/classes.tsx new file mode 100644 index 00000000..1847b167 --- /dev/null +++ b/apps/frontend/src/pages/our-programs/classes/classes.tsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react'; +import headerImage from '../../../assets/images/our-programs/classes/header-image.png'; +import ClassDescription from '../../../components/our-programs/classes/class-description.tsx'; +import ClassHero from '../../../components/our-programs/classes/class-hero.tsx'; +import ClassSignup from '../../../components/our-programs/classes/class-signup.tsx'; + +function Classes() { + const [heroImageURL, setHeroImageURL] = useState(undefined); + + useEffect(() => { + fetch(`/images/10`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setHeroImageURL(data.bucket_link); + } else { + setHeroImageURL(headerImage); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setHeroImageURL(headerImage); + }); + }, []); + + return ( +
+ Classes - The Recyclery + + + +
+ ); +} + +export default Classes; diff --git a/apps/frontend/src/pages/our-programs/freecyclery/freecyclery.tsx b/apps/frontend/src/pages/our-programs/freecyclery/freecyclery.tsx new file mode 100644 index 00000000..521fb0ae --- /dev/null +++ b/apps/frontend/src/pages/our-programs/freecyclery/freecyclery.tsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from 'react'; +import headerPoster from '../../../assets/images/our-programs/freecyclery/freecyclery-header.png'; +import earnABike from '../../../assets/images/our-programs/freecyclery/earn-a-bike.jpg'; +import { BgImage } from '../../../components/generic/bg-image.tsx'; +import { H1 } from '../../../components/generic/styled-tags.tsx'; +import AboutSection from '../../../components/our-programs/freecyclery/about-section.tsx'; +import EarnABike from '../../../components/our-programs/freecyclery/earn-a-bike.tsx'; +import HowItWorks from '../../../components/our-programs/freecyclery/how-it-works.tsx'; +import MakeReferral from '../../../components/our-programs/freecyclery/make-referral.tsx'; +import Partners from '../../../components/our-programs/freecyclery/partners.tsx'; + +function Freecyclery() { + const [heroimageURL, setHeroImageURL] = useState(undefined); + const [secimageURL, setSecImageURL] = useState(undefined); + + useEffect(() => { + fetch(`/images/7`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setHeroImageURL(data.bucket_link); + } else { + setHeroImageURL(headerPoster); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setHeroImageURL(headerPoster); + }); + }, []); + + useEffect(() => { + fetch(`/images/8`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setSecImageURL(data.bucket_link); + } else { + setSecImageURL(earnABike); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + }); + }, []); + + return ( +
+ Freecyclery - The Recyclery + +

freecyclery

+

+ Earn a free bike through our referral and fellowship programs. +

+
+ + + + + +
+ ); +} + +export default Freecyclery; diff --git a/apps/frontend/src/pages/our-programs/openshop/openshop.tsx b/apps/frontend/src/pages/our-programs/openshop/openshop.tsx new file mode 100644 index 00000000..6d07e723 --- /dev/null +++ b/apps/frontend/src/pages/our-programs/openshop/openshop.tsx @@ -0,0 +1,103 @@ +import { useEffect, useState } from 'react'; +import OpenshopHero from '../../../assets/images/our-programs/openshop/openshop-hero.png'; +import OpenshopSection1 from '../../../assets/images/our-programs/openshop/openshop-section-1.png'; +import { BgImage } from '../../../components/generic/bg-image.tsx'; +import { A, H1, H2, Section } from '../../../components/generic/styled-tags.tsx'; + +function OpenShop() { + const [heroimageURL, setHeroImageURL] = useState(undefined); + const [secimageURL, setSecImageURL] = useState(undefined); + + useEffect(() => { + fetch(`/images/5`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setHeroImageURL(data.bucket_link); + } else { + setHeroImageURL(OpenshopHero); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setHeroImageURL(OpenshopHero); + }); + }, []); + + useEffect(() => { + fetch(`/images/6`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setSecImageURL(data.bucket_link); + } else { + setSecImageURL(OpenshopSection1); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setSecImageURL(OpenshopSection1); + }); + }, []); + + return ( +
+ Open Shop - The Recyclery + +

open shop

+

+ Use our tools to fix your own bike with the instruction of Recyclery mechanics. +

+
+
+
+
+

what is open shop?

+
+

+ Open Shop is a program where you can work on your own bike with help from Recyclery + mechanics. All levels of experience are welcome. We strive to provide an open, + respectful and collaborative atmosphere free of mechanical elitism. One of our + volunteers will be happy to help you figure out how to get your bike running well. +

+

+ For those working on their own bicycles we suggest a $10/hour donation. This helps + us covering running costs like rent, tools. +

+

+ You may purchase new or used parts for affordable prices during Open Shop. +

+

+ For femme, trans, women and non-binary community,{' '} + FTWN-B Shop Time (Sundays 3-6pm) is another + opportunity to work on your bike. +

+
+
+
+ Two people working on a bike +
+
+

hours

+
+
    +
  • Tuesdays 5-7pm
  • +
  • Thursdays 7-9pm
  • +
  • Saturdays 11am-2pm
  • +
+

+ Complete bikes are sold during Saturday Open Shop 11am - 2pm, our In-Person Sales + Wednesday 6pm - 8pm, and 24/7 through our{' '} + online shop. +

+
+
+
+ ); +} + +export default OpenShop; diff --git a/apps/frontend/src/pages/signup.tsx b/apps/frontend/src/pages/signup.tsx index c7fc128b..72d8d48d 100644 --- a/apps/frontend/src/pages/signup.tsx +++ b/apps/frontend/src/pages/signup.tsx @@ -1,40 +1,8 @@ -import { useState, FormEvent, ChangeEvent } from 'react'; +import { ChangeEvent, FormEvent, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { styled } from 'styled-components'; -import { Form } from '../components/form/form'; -import { Input } from '../components/form/input'; -import { RedSpan } from '../components/form/styles'; import type { SignupRequest } from '../../types/auth.ts'; - -const SignupContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 80vh; - padding: 2rem 1rem; -`; - -const StyledLink = styled(Link)` - color: #646cff; - text-decoration: none; - font-size: 0.9rem; - - &:hover { - text-decoration: underline; - } -`; - -const LoginPrompt = styled.div` - margin-top: 16px; - text-align: center; -`; - -const ErrorMessage = styled(RedSpan)` - text-align: center; - display: block; - margin-bottom: 8px; -`; +import { Form } from '../components/form/form.tsx'; +import { Input } from '../components/form/input.tsx'; export default function Signup() { const navigate = useNavigate(); @@ -98,7 +66,8 @@ export default function Signup() { }; return ( - +
+ Sign Up - The Recyclery
- {error && {error}} + {error &&

{error}

} - - Already have an account? Log in - +
+ Already have an account?{' '} + + Log in + +
- +
); } diff --git a/apps/frontend/src/pages/support-us/contribute-financially/contribute-financially.tsx b/apps/frontend/src/pages/support-us/contribute-financially/contribute-financially.tsx new file mode 100644 index 00000000..effac265 --- /dev/null +++ b/apps/frontend/src/pages/support-us/contribute-financially/contribute-financially.tsx @@ -0,0 +1,93 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import ContributeHero from '../../../assets/images/support-us/contribute-financially/contribute-hero.png'; +import ContributeImage from '../../../assets/images/support-us/contribute-financially/contribute-image.png'; +import Accordion from '../../../components/generic/accordion.tsx'; +import { BgImage } from '../../../components/generic/bg-image.tsx'; +import { Button } from '../../../components/generic/buttons.tsx'; +import DashedBorder from '../../../components/generic/dashed-border.tsx'; +import { H1, H2, H3, Section } from '../../../components/generic/styled-tags.tsx'; +import { donationAccordionContent } from '../../../content/donation-info.ts'; + +function ContributeFinancially() { + const [heroImageURL, setHeroImageURL] = useState(undefined); + const [contributeImageURL, setContributeImageURL] = useState(undefined); + + useEffect(() => { + fetch(`/images/11`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setHeroImageURL(data.bucket_link); + } else { + setHeroImageURL(ContributeHero); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setHeroImageURL(ContributeHero); + }); + }, []); + + useEffect(() => { + fetch(`/images/12`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setContributeImageURL(data.bucket_link); + } else { + setContributeImageURL(ContributeImage); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setHeroImageURL(ContributeImage); + }); + }, []); + + return ( +
+ Contribute Financially - The Recyclery + +

contribute financially

+

+ Donate money directly to help fund our programs +

+
+
+

make a donation today

+
+ +
+

how does my donation help?

+

+ Click on one of the options below to see what your donation amount directly + correlates to in our programs. +

+ +
+
+

ready to donate?

+

+ Regardless of how much you are able or willing to donate, we greatly appreciate your + support! +

+ donation image + +
+
+
+
+
+ ); +} + +export default ContributeFinancially; diff --git a/apps/frontend/src/pages/support-us/donate-a-bike/donate-a-bike.tsx b/apps/frontend/src/pages/support-us/donate-a-bike/donate-a-bike.tsx new file mode 100644 index 00000000..2a2b10d4 --- /dev/null +++ b/apps/frontend/src/pages/support-us/donate-a-bike/donate-a-bike.tsx @@ -0,0 +1,132 @@ +import { useEffect, useState } from 'react'; +import DonateHero from '../../../assets/images/support-us/donate-a-bike/donate-hero.png'; +import DonateImage from '../../../assets/images/support-us/donate-a-bike/donate-image.jpeg'; +import { BgImage } from '../../../components/generic/bg-image.tsx'; +import DashedBorder from '../../../components/generic/dashed-border.tsx'; +import { A, H1, H2, H3, Section } from '../../../components/generic/styled-tags.tsx'; +import { collectionPoints } from '../../../content/collection-points.ts'; + +function DonateABike() { + const [heroImageURL, setHeroImageURL] = useState(undefined); + const [donateImageURL, setDonateImageURL] = useState(undefined); + + useEffect(() => { + fetch(`/images/13`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setHeroImageURL(data.bucket_link); + } else { + setHeroImageURL(DonateHero); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setHeroImageURL(DonateHero); + }); + }, []); + + useEffect(() => { + fetch(`/images/14`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setDonateImageURL(data.bucket_link); + } else { + setDonateImageURL(DonateImage); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setDonateImageURL(DonateImage); + }); + }, []); + + return ( +
+ Donate a Bike - The Recyclery + +

donate a bike

+

+ Donate your old bikes to either support our programs or help people in need +

+
+
+
+

why donate a bike

+

+ A donated bicycle will be given away to people who need them through our{' '} + Freecyclery program, used in our{' '} + classes, or repaired by a mechanic and sold for an + affordable price. We rely on these donations to keep all of our programs running. +

+
+
+ why donate image +
+
+
+

how to donate

+

+ We offer three different ways for you to donate your old bikes. Feel free to choose the + method that is the most convenient for you. +

+
+
+ +

1. public programs

+

+ Bring in your donation during our public program hours listed below. +

+
    +
  • Saturday: 11am - 2pm
  • +
  • Monday: 12pm - 3pm and 5:30pm - 7:30pm
  • +
  • Tuesday: 5pm - 7pm
  • +
  • Wednesday: 6pm - 8pm
  • +
  • Thursday: 10am - 1pm and 7pm - 9pm
  • +
+
+
+
+
+ +

2. email us

+

+ Send an email to{' '} + donatebikes@therecyclery.org and + we'll reach out to you about scheduling a pick-up. +

+
+
+
+ +

3. direct dropoff

+

+ Drop off bikes directly to the collection points listed in the section below. +

+
+
+
+
+
+
+

collection points

+
    + {collectionPoints.map((collectionPoint, index) => ( +
  • + {collectionPoint.title} +

    {collectionPoint.address}

    +

    {collectionPoint.phone}

    +
  • + ))} +
+
+
+ ); +} + +export default DonateABike; diff --git a/apps/frontend/src/pages/support-us/donate-time/donate-time.tsx b/apps/frontend/src/pages/support-us/donate-time/donate-time.tsx new file mode 100644 index 00000000..72d4cd8b --- /dev/null +++ b/apps/frontend/src/pages/support-us/donate-time/donate-time.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react'; +import headerImage from '../../../assets/images/support-us/donate-time/donate-time-header.jpg'; +import volunteerFun from '../../../assets/images/support-us/donate-time/volunteer-fun.jpg'; +import HeroSection from '../../../components/support-us/donate-time/hero-section.tsx'; +import Roles from '../../../components/support-us/donate-time/roles.tsx'; +import VolunteerMission from '../../../components/support-us/donate-time/volunteer-mission.tsx'; + +function DonateTime() { + const [heroImageURL, setHeroImageURL] = useState(undefined); + const [volunteerImageURL, setVolunteerImageURL] = useState(undefined); + + useEffect(() => { + fetch(`/images/15`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setHeroImageURL(data.bucket_link); + } else { + setHeroImageURL(headerImage); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setHeroImageURL(headerImage); + }); + }, []); + + useEffect(() => { + fetch(`/images/16`) + .then(res => res.json()) + .then(data => { + if (data?.bucket_link) { + setVolunteerImageURL(data.bucket_link); + } else { + setVolunteerImageURL(volunteerFun); + } + }) + .catch(err => { + console.error('image URL was not fetched correctly', err); + setVolunteerImageURL(volunteerFun); + }); + }, []); + + return ( + <> + Become a Volunteer - The Recyclery + + + + + ); +} + +export default DonateTime; diff --git a/apps/frontend/src/pages/support-us/our-supporters/our-supporters.tsx b/apps/frontend/src/pages/support-us/our-supporters/our-supporters.tsx new file mode 100644 index 00000000..245fe2fc --- /dev/null +++ b/apps/frontend/src/pages/support-us/our-supporters/our-supporters.tsx @@ -0,0 +1,61 @@ +import { H1, H2, H3, Section } from '../../../components/generic/styled-tags.tsx'; +import { Award, DollarSign } from 'lucide-react'; +import { Button } from '../../../components/generic/buttons.tsx'; +import { Link } from 'react-router-dom'; + +function OurSupporters() { + return ( + <> + Our Supporters - The Recyclery +
+
+

our sponsors

+

a special thanks to our supporters

+

since 2024

+
+
+ +
+
+
+
+ +

gold ($5000+)

+

Allstate Foundation

+
+
+ +

silver ($2500+)

+

New Belgium Brewing House

+
+
+ +

bronze ($1000+)

+

+ Ouweleen Family +
Susan Booth and Max Leventhal +
Dan Engel & Family +

+
+
+
+

want to become a sponsor?

+

join us today!

+
+ + +
+
+ + ); +} + +export default OurSupporters; diff --git a/apps/frontend/src/pages/upload/Upload.tsx b/apps/frontend/src/pages/upload/Upload.tsx new file mode 100644 index 00000000..33ed31bc --- /dev/null +++ b/apps/frontend/src/pages/upload/Upload.tsx @@ -0,0 +1,216 @@ +import React, { ChangeEvent, DragEvent, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { styled } from 'styled-components'; +import drop from '../../assets/images/upload/drop.png'; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + max-width: 600px; + margin: 0 auto; + padding: 20px; + text-align: center; +`; + +const Title = styled.h2` + font-family: Montserrat, sans-serif; + font-weight: 700; + margin-top: 10px; + margin-bottom: 10px; + font-size: 24px; +`; + +const UploadBox = styled.div` + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 15px; + border: 1px solid #ccc; + width: 100%; + max-width: 465px; + background-color: white; + padding: 20px; + margin-top: 10px; + height: 400px; + display: flex; + flex-direction: column; + justify-content: center; +`; + +const DragArea = styled.div` + border: 2px dashed #ccc; + padding: 20px; + text-align: center; + border-radius: 15px; + cursor: pointer; + background-color: #f0f0f0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; /* Ensure it takes the full width of the parent */ + height: 100%; /* Set height equal to width to make it square */ + aspect-ratio: 1; /* Maintain a 1:1 aspect ratio */ +`; + +const DropImage = styled.img` + width: 60px; + height: 60px; + margin-bottom: 0px; +`; + +const Paragraph = styled.p` + font-family: Montserrat, sans-serif; + margin-bottom: 10px; + font-size: 14px; +`; + +const ButtonGroup = styled.div` + display: flex; + justify-content: center; + margin-top: 20px; + color: #fff; +`; + +const ConfirmButton = styled.button` + background-color: #000; + color: white; + height: 40px; + width: 120px; + border-radius: 20px; + margin-right: 20px; + text-align: center; + font-family: Montserrat, sans-serif; + font-size: 16px; + font-weight: 600; + cursor: pointer; + &:hover { + background-color: #333; + } +`; + +const CancelButton = styled.button` + background-color: white; + color: black; + height: 40px; + width: 120px; + border-radius: 20px; + cursor: pointer; + &:hover { + background-color: #f0f0f0; + } + text-align: center; + font-family: Montserrat, sans-serif; + font-size: 16px; + font-weight: 600; +`; + +const Upload: React.FC = () => { + const [file, setFile] = useState(null); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [dragActive, setDragActive] = useState(false); + const { id } = useParams<{ id: string }>(); + const handleDrag = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === 'dragenter' || e.type === 'dragover') { + setDragActive(true); + } else if (e.type === 'dragleave') { + setDragActive(false); + } + }; + + const handleDrop = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + setFile(e.dataTransfer.files[0]); + } + }; + + const handleChange = (e: ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + setFile(e.target.files[0]); + } + }; + + const handleUpload = async () => { + if (!file) { + alert('Please select a file to upload.'); + return; + } + const formData = new FormData(); + formData.append('file', file); + formData.append('fileName', file.name); + + + try { + const response = await fetch(`${import.meta.env.VITE_BACKEND_URL}/upload/${id}`, { // Replace '2' with the actual ID + method: 'PUT', + body: formData, + }); + console.log('Response status:', response.status); // Log response status + const responseData = await response.json(); + console.log('Response data:', responseData); // Log response data + if (!response.ok) { + throw new Error('Failed to upload file'); + } + + alert('File uploaded successfully!'); + setFile(null); + } catch (error) { + alert( error ); + } + }; + + return ( + + DROP YOUR FILE HERE + + + + + Drag and drop your file here, or click to browse. (jpg, jpeg, heic, png) + + + + + + + {file && ( +
+

Selected File: {file.name}

+
+ )} + + + Upload + { + setFile(null); + setDragActive(false); + }} + > + Cancel + + +
+ ); +}; + +export default Upload; diff --git a/apps/frontend/src/pages/upload/UploadHours.tsx b/apps/frontend/src/pages/upload/UploadHours.tsx new file mode 100644 index 00000000..c74f201b --- /dev/null +++ b/apps/frontend/src/pages/upload/UploadHours.tsx @@ -0,0 +1,104 @@ +import { FormEvent, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { styled } from 'styled-components'; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + max-width: 800px; + margin: 0 auto; + padding: 20px; + text-align: center; +`; + +const Title = styled.h2` + font-family: Montserrat, sans-serif; + font-weight: 700; + margin-top: 10px; + margin-bottom: 10px; + font-size: 24px; +`; +const TextArea = styled.textarea` width:100%; height:300px; padding:8px; `; +const ButtonGroup = styled.div` + display: flex; + justify-content: center; + margin-top: 20px; + color: #fff; +`; + +const ConfirmButton = styled.button` + background-color: #000; + color: white; + height: 40px; + width: 120px; + border-radius: 20px; + margin-right: 20px; + text-align: center; + font-family: Montserrat, sans-serif; + font-size: 16px; + font-weight: 600; + cursor: pointer; + &:hover { + background-color: #333; + } +`; + +const CancelButton = styled.button` + background-color: white; + color: black; + height: 40px; + width: 120px; + border-radius: 20px; + cursor: pointer; + &:hover { + background-color: #f0f0f0; + } + text-align: center; + font-family: Montserrat, sans-serif; + font-size: 16px; + font-weight: 600; +`; + +export default function UploadHours() { + const [hours, setHours] = useState(''); + const { id } = useParams<{ id: string }>(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!hours) { + alert('Please enter hours text'); + return; + } + const res = await fetch(`${import.meta.env.VITE_BACKEND_URL}/uploadhours/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hours }), + }); + const data = await res.json(); + if (!res.ok) { + alert(data.error || 'Failed to update hours'); + } else { + alert('Hours updated successfully'); + } + }; + + return ( + + Update Hours +
+