diff --git a/.gitignore b/.gitignore index cb319e2..a7bf959 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ with-nextjs/out # Websockets */backend/.env +backend/.env # local env files */.env*.local diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..5a05ecb --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "es5" +} diff --git a/backend/controllers/clubsController.js b/backend/controllers/clubsController.js new file mode 100644 index 0000000..4cc4b10 --- /dev/null +++ b/backend/controllers/clubsController.js @@ -0,0 +1,88 @@ +import supabaseConnection from '../database.js'; +import clubs from '../models/clubs.js'; + +const clubsController = { + getAllClubs: async (req, res) => { + try { + const data = await clubs.getAllClubs(); + res.json(data); + } catch (err) { + res.status(500).json({ error: err.message }); + } + }, + getClubById: async (req, res) => { + try { + if (!req.params?.id) { + return res.status(400).json({ message: 'Club ID is required' }); + } + const data = await clubs.getClubById(req.params.id); + if (!data) { + return res.status(404).json({ message: 'Club not found' }); + } + res.json(data); + } catch (error) { + console.error('Error fetching clubs info:', error); + res.status(500).json({ message: 'Internal Server Error' }); + } + }, + getClubByAccountId: async (req, res) => { + try { + if (!req.params?.id) { + return res.status(400).json({ message: 'Club ID is required' }); + } + const data = await clubs.getClubByAccountId(req.params.id); + if (!data) { + return res.status(404).json({ message: 'Club not found' }); + } + res.json(data); + } catch (error) { + console.error('Error fetching clubs info:', error); + res.status(500).json({ message: 'Internal Server Error' }); + } + }, + updateClub: async (req, res) => { + try { + if (!req.params?.id) { + return res.status(400).json({ message: 'Club ID is required' }); + } + const data = {}; + const allowedFields = [ + 'name', + 'location', + 'campus', + 'category', + 'description', + ]; + for (const field of allowedFields) { + if (req.body[field] !== undefined) { + data[field] = req.body[field]; + } + } + await clubs.updateClubInfo(req.params.id, data); + res + .status(200) + .json({ message: 'Club information updated successfully', data }); + } catch (error) { + console.error('Error updating club info:', error); + res.status(500).json({ message: 'Internal Server Error' }); + } + }, + + getEventsClub: async (req, res) => { + try { + const clubId = req.params?.id; + if (!clubId) { + return res.status(400).json({ message: 'Club ID is required' }); + } + const events = await supabaseConnection + .from('events') + .select('*') + .eq('hostClubId', clubId); + res.status(200).json(events); + } catch (error) { + console.error('Error updating club info:', error); + res.status(500).json({ message: 'Internal Server Error' }); + } + }, +}; +export default clubsController; diff --git a/backend/controllers/eventsController.js b/backend/controllers/eventsController.js new file mode 100644 index 0000000..117690f --- /dev/null +++ b/backend/controllers/eventsController.js @@ -0,0 +1,51 @@ +import supabaseConnection from '../database.js'; + +export const createEvent = async (req, res) => { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + + const { + title, + description, + hostClubId, + startTimestamp, + endTimestamp, + event_status, + } = req.body; + + const { data, error } = await supabaseConnection.from('events').insert([ + { + title, + description, + hostClubId: '4', // Hardcoded for now + startTimestamp, + endTimestamp, + event_status, + }, + ]); + + if (error) { + return res.status(500).json({ error: error.message }); + } + + return res.status(201).json({ data }); +}; + +export const getEvents = async (req, res) => { + const { data, error } = await supabaseConnection.from('events').select(` + *, + hostClub: clubs ( + id, + name, + description, + verified + ) + `); + + if (error) { + return res.status(500).json({ error: error.message }); + } + + return res.json(data); +}; diff --git a/backend/controllers/usersController.js b/backend/controllers/usersController.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/database.js b/backend/database.js new file mode 100644 index 0000000..a2659c9 --- /dev/null +++ b/backend/database.js @@ -0,0 +1,9 @@ +import 'dotenv/config'; +import { createClient } from '@supabase/supabase-js'; + +const supabaseConnection = createClient( + process.env.EXPO_PUBLIC_SUPABASE_URL, + process.env.PRIVATE_API_KEY +); + +export default supabaseConnection; diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs new file mode 100644 index 0000000..468df2b --- /dev/null +++ b/backend/eslint.config.mjs @@ -0,0 +1,25 @@ +// ESLint config for Node.js backend (CommonJS/ESM) +export default [ + { + files: ['**/*.js'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + require: 'readonly', + module: 'readonly', + __dirname: 'readonly', + process: 'readonly', + console: 'readonly', + }, + }, + rules: { + 'no-unused-vars': 'warn', + 'no-undef': 'error', + semi: ['error', 'always'], + quotes: ['error', 'single'], + eqeqeq: 'warn', + }, + ignores: ['node_modules/*', 'dist/*'], + }, +]; diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 0000000..187e5ed --- /dev/null +++ b/backend/index.js @@ -0,0 +1,44 @@ +import express from 'express'; +import cors from 'cors'; +import supabaseConnection from './database.js'; +import clubsRouter from './routes/clubsRouter.js'; +import { eventsRouter } from './routes/eventsRouter.js'; + +const app = express(); +const PORT = 3000; + +app.use(express.json()); + +app.get('/', (req, res) => { + res.send('API running'); +}); + +// Middleware +const corsConfig = { + origin: 'http://localhost:8081', + credentials: true, +}; + +app.use(cors(corsConfig)); + +app.use('/api/events', eventsRouter); +app.use('/api/clubs', clubsRouter); + +const testSupabase = async () => { + console.log('Testing Supabase connection...'); + + const { data, error } = await supabaseConnection.from('Test').select('*'); + + if (error) { + console.error('Supabase error:', error.message); + } else if (!data || data.length === 0) { + console.warn('No data found in Test table.'); + } else { + console.log('Data from Supabase:', data); + } +}; + +testSupabase(); +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); diff --git a/backend/models/clubs.js b/backend/models/clubs.js new file mode 100644 index 0000000..ffd47e6 --- /dev/null +++ b/backend/models/clubs.js @@ -0,0 +1,40 @@ +import supabaseConnection from '../database.js'; + +const clubModel = { + getAllClubs: async () => { + const { data, error } = await supabaseConnection.from('clubs').select('*'); + if (error) throw new Error(error.message); + return data || []; + }, + getClubById: async (id) => { + const { data, error } = await supabaseConnection + .from('clubs') + .select('*') + .eq('id', id) + .maybeSingle(); + if (error) throw new Error(error.message); + return data; + }, + getClubByAccountId: async (accountId) => { + const { data, error } = await supabaseConnection + .from('clubs') + .select('*') + .eq('accountId', accountId) + .maybeSingle(); + if (error) throw new Error(error.message); + return data; + }, + updateClubInfo: async (id, updateData) => { + const { data, error } = await supabaseConnection + .from('clubs') + .update(updateData) + .eq('accountId', id); + + if (error) { + throw new Error(error.message); + } + return data; + }, +}; + +export default clubModel; diff --git a/backend/models/user.js b/backend/models/user.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..76cb6a0 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,28 @@ +{ + "name": "backend", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint . --config eslint.config.mjs --fix", + "format": "prettier --write ." + }, + "type": "module", + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@supabase/supabase-js": "^2.50.0", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^5.1.0" + }, + "devDependencies": { + "@types/next": "^8.0.7", + "eslint": "^9.29.0", + "next": "^15.3.3", + "prettier": "^3.5.3" + } +} diff --git a/backend/routes/clubsRouter.js b/backend/routes/clubsRouter.js new file mode 100644 index 0000000..47052b1 --- /dev/null +++ b/backend/routes/clubsRouter.js @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import clubsController from '../controllers/clubsController.js'; + +const router = Router(); + +router.get('/', clubsController.getAllClubs); +router.get('/me/:id', clubsController.getClubByAccountId); +router.get('/:id', clubsController.getClubById); +router.patch('/:id', clubsController.updateClub); +router.get('/:id/events', clubsController.getEventsClub); + +export default router; diff --git a/backend/routes/eventsRouter.js b/backend/routes/eventsRouter.js new file mode 100644 index 0000000..46624e3 --- /dev/null +++ b/backend/routes/eventsRouter.js @@ -0,0 +1,7 @@ +import express from 'express'; +import { createEvent, getEvents } from '../controllers/eventsController.js'; + +export const eventsRouter = express.Router(); + +eventsRouter.post('/createEvent', createEvent); +eventsRouter.get('/', getEvents); diff --git a/backend/routes/usersRouter.js b/backend/routes/usersRouter.js new file mode 100644 index 0000000..e69de29 diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..f69a872 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,13 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import pluginReact from "eslint-plugin-react"; +import { defineConfig } from "eslint/config"; + + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], plugins: { js }, extends: ["js/recommended"] }, + { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], languageOptions: { globals: globals.browser } }, + tseslint.configs.recommended, + pluginReact.configs.flat.recommended, +]); diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..acf3c2e --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local +.env +# typescript +*.tsbuildinfo + +.vscode + +app-example diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..5a05ecb --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "es5" +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..48dd63f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,50 @@ +# Welcome to your Expo app 👋 + +This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). + +## Get started + +1. Install dependencies + + ```bash + npm install + ``` + +2. Start the app + + ```bash + npx expo start + ``` + +In the output, you'll find options to open the app in a + +- [development build](https://docs.expo.dev/develop/development-builds/introduction/) +- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) +- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) +- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo + +You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). + +## Get a fresh project + +When you're ready, run: + +```bash +npm run reset-project +``` + +This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. + +## Learn more + +To learn more about developing your project with Expo, look at the following resources: + +- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). +- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. + +## Join the community + +Join our community of developers creating universal apps. + +- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. +- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. diff --git a/frontend/app.json b/frontend/app.json new file mode 100644 index 0000000..5904589 --- /dev/null +++ b/frontend/app.json @@ -0,0 +1,43 @@ +{ + "expo": { + "name": "frontend", + "slug": "frontend", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "frontend", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/images/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "edgeToEdgeEnabled": true, + "package": "com.bobth.frontend" + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-splash-screen", + { + "image": "./assets/images/splash-icon.png", + "imageWidth": 200, + "resizeMode": "contain", + "backgroundColor": "#ffffff" + } + ] + ], + "experiments": { + "typedRoutes": true + } + } +} diff --git a/frontend/app/(home)/index.tsx b/frontend/app/(home)/index.tsx new file mode 100644 index 0000000..59786f7 --- /dev/null +++ b/frontend/app/(home)/index.tsx @@ -0,0 +1,23 @@ +import 'react-native-url-polyfill/auto'; +import { useState, useEffect } from 'react'; +import { supabase } from '../../lib/supabase'; +import Auth from '../../components/Auth'; +import { View, Text } from 'react-native'; +import { Session } from '@supabase/supabase-js'; +export default function App() { + const [session, setSession] = useState(null); + useEffect(() => { + supabase.auth.getSession().then(({ data: { session } }) => { + setSession(session); + }); + supabase.auth.onAuthStateChange((_event, session) => { + setSession(session); + }); + }, []); + return ( + + + {session && session.user && {session.user.id}} + + ); +} diff --git a/frontend/app/_layout.tsx b/frontend/app/_layout.tsx new file mode 100644 index 0000000..4c2adfc --- /dev/null +++ b/frontend/app/_layout.tsx @@ -0,0 +1,22 @@ +import { Stack, useRouter } from 'expo-router'; +import { TouchableOpacity, Text } from 'react-native'; +import { AuthProvider } from '../context/AuthContext'; +import { HeaderRight } from '../components/home/HeaderRight'; + +export default function RootLayout() { + const router = useRouter(); + + return ( + + , + }} + /> + + ); +} diff --git a/frontend/app/auth/login/index.tsx b/frontend/app/auth/login/index.tsx new file mode 100644 index 0000000..17d96fd --- /dev/null +++ b/frontend/app/auth/login/index.tsx @@ -0,0 +1,10 @@ +import 'react-native-url-polyfill/auto'; +import Login from '../../../components/login/Login'; +import { View, Text } from 'react-native'; +export default function App() { + return ( + + + + ); +} diff --git a/frontend/app/auth/signup/index.tsx b/frontend/app/auth/signup/index.tsx new file mode 100644 index 0000000..bd58b14 --- /dev/null +++ b/frontend/app/auth/signup/index.tsx @@ -0,0 +1,10 @@ +import 'react-native-url-polyfill/auto'; +import SignUp from '../../../components/signup/SignUp'; +import { View, Text } from 'react-native'; +export default function App() { + return ( + + + + ); +} diff --git a/frontend/app/club/[id].tsx b/frontend/app/club/[id].tsx new file mode 100644 index 0000000..761a99f --- /dev/null +++ b/frontend/app/club/[id].tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import ClubPageUserView from '../../components/club/ClubPageUserView'; +import { useLocalSearchParams } from 'expo-router'; + +export default function ClubPage() { + const { id } = useLocalSearchParams(); + return ; +} diff --git a/frontend/app/club/edit_club_info.tsx b/frontend/app/club/edit_club_info.tsx new file mode 100644 index 0000000..4317054 --- /dev/null +++ b/frontend/app/club/edit_club_info.tsx @@ -0,0 +1,33 @@ +import { Text, View } from 'react-native'; +import { EditClub } from '../../components/club/EditClub'; +import { useAuth } from '../../context/AuthContext'; + +export default function Club() { + const { session, accountType } = useAuth(); + if (!session || !session.user || accountType !== 'club') { + return ( + + + You must be logged in as a club to edit your club information. + + + ); + } + return ( + + + + ); +} diff --git a/frontend/app/club/index.tsx b/frontend/app/club/index.tsx new file mode 100644 index 0000000..5b801b6 --- /dev/null +++ b/frontend/app/club/index.tsx @@ -0,0 +1,51 @@ +import React, { useEffect, useState } from 'react'; +import { + View, + Text, + TouchableOpacity, + ScrollView, + ActivityIndicator, +} from 'react-native'; +import { useRouter } from 'expo-router'; + +export default function ClubsList() { + const [clubs, setClubs] = useState([]); + const [loading, setLoading] = useState(true); + const router = useRouter(); + + useEffect(() => { + fetch('http://localhost:3000/api/clubs') + .then((res) => res.json()) + .then((data) => { + setClubs(data); + setLoading(false); + }) + .catch((err) => { + setLoading(false); + }); + }, []); + + if (loading) + return ; + + return ( + + + Clubs + + {clubs.map((club) => ( + router.push(`/club/${club.id}`)} + > + {club.name} + + ))} + + ); +} diff --git a/frontend/app/create-event/index.tsx b/frontend/app/create-event/index.tsx new file mode 100644 index 0000000..1eba1da --- /dev/null +++ b/frontend/app/create-event/index.tsx @@ -0,0 +1,49 @@ +import EventForm from '../../components/Events/EventForm'; +import { useAuth } from '../../context/AuthContext'; +import { useRouter } from 'expo-router'; +import { useEffect, useState } from 'react'; +import ErrorPage from '../../components/ErrorPage'; + +export default function CreateEventPage() { + const { accountType } = useAuth(); + const router = useRouter(); + const [showDenied, setShowDenied] = useState(false); + + useEffect(() => { + if (accountType && accountType !== 'club') { + setShowDenied(true); + } + }, [accountType]); + + if (!accountType) { + return null; + } + + if (showDenied) { + return ( + + ); + } + + return ( +
+

+ Create Event +

+ +
+ ); +} diff --git a/frontend/app/dashboard/index.tsx b/frontend/app/dashboard/index.tsx new file mode 100644 index 0000000..a821e09 --- /dev/null +++ b/frontend/app/dashboard/index.tsx @@ -0,0 +1,11 @@ +import { useState, useEffect } from 'react'; +import { FlatList, Text, View } from 'react-native'; +import { useAuth } from '@/context'; +import ClubDashboardEventsList from '../../components/club/dashboard/ClubDashboardEventsList'; +export default function Dashboard() { + return ( + + + + ); +} diff --git a/frontend/app/index.tsx b/frontend/app/index.tsx new file mode 100644 index 0000000..7410f05 --- /dev/null +++ b/frontend/app/index.tsx @@ -0,0 +1,31 @@ +import { Text, View } from 'react-native'; +import { useAuth } from '../context/AuthContext'; +import Feed from '../components/feed/feed.js'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +export default function Index() { + const { session, accountType } = useAuth(); + + return ( + + + {session && session.user ? ( + <> + + Welcome, {session.user.email}! + + + Account Type:{' '} + {accountType === 'club' ? 'Club Account' : 'User Account'} + + + + ) : ( + + Please log in or sign up. + + )} + + + ); +} diff --git a/frontend/assets/fonts/SpaceMono-Regular.ttf b/frontend/assets/fonts/SpaceMono-Regular.ttf new file mode 100644 index 0000000..28d7ff7 Binary files /dev/null and b/frontend/assets/fonts/SpaceMono-Regular.ttf differ diff --git a/frontend/assets/images/adaptive-icon.png b/frontend/assets/images/adaptive-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/frontend/assets/images/adaptive-icon.png differ diff --git a/frontend/assets/images/favicon.png b/frontend/assets/images/favicon.png new file mode 100644 index 0000000..e75f697 Binary files /dev/null and b/frontend/assets/images/favicon.png differ diff --git a/frontend/assets/images/icon.png b/frontend/assets/images/icon.png new file mode 100644 index 0000000..a0b1526 Binary files /dev/null and b/frontend/assets/images/icon.png differ diff --git a/frontend/assets/images/partial-react-logo.png b/frontend/assets/images/partial-react-logo.png new file mode 100644 index 0000000..66fd957 Binary files /dev/null and b/frontend/assets/images/partial-react-logo.png differ diff --git a/frontend/assets/images/react-logo.png b/frontend/assets/images/react-logo.png new file mode 100644 index 0000000..9d72a9f Binary files /dev/null and b/frontend/assets/images/react-logo.png differ diff --git a/frontend/assets/images/react-logo@2x.png b/frontend/assets/images/react-logo@2x.png new file mode 100644 index 0000000..2229b13 Binary files /dev/null and b/frontend/assets/images/react-logo@2x.png differ diff --git a/frontend/assets/images/react-logo@3x.png b/frontend/assets/images/react-logo@3x.png new file mode 100644 index 0000000..a99b203 Binary files /dev/null and b/frontend/assets/images/react-logo@3x.png differ diff --git a/frontend/assets/images/splash-icon.png b/frontend/assets/images/splash-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/frontend/assets/images/splash-icon.png differ diff --git a/frontend/components/Auth.tsx b/frontend/components/Auth.tsx new file mode 100644 index 0000000..5e37ffb --- /dev/null +++ b/frontend/components/Auth.tsx @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import { Alert, StyleSheet, View } from 'react-native'; +import { supabase } from '../lib/supabase'; +import { Button, Input, CheckBox } from '@rneui/base'; + +export default function Auth() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + + async function signInWithEmail() { + setLoading(true); + const { error } = await supabase.auth.signInWithPassword({ + email: email, + password: password, + }); + + if (error) { + console.log('Supabase Auth Error:', error); + Alert.alert( + 'Error', + `${error.message}\nStatus: ${error.status || ''}\n${JSON.stringify(error)}` + ); + } + setLoading(false); + } + + async function signUpWithEmail() { + setLoading(true); + const { + data: { session }, + error, + } = await supabase.auth.signUp({ + email: email, + password: password, + }); + + if (error) { + console.log('Supabase Auth Error:', error); + Alert.alert( + 'Error', + `${error.message}\nStatus: ${error.status || ''}\n${JSON.stringify(error)}` + ); + } + if (!session) + Alert.alert('Please check your inbox for email verification!'); + setLoading(false); + } + + return ( + + + setEmail(text)} + value={email} + placeholder="email@address.com" + autoCapitalize={'none'} + /> + + + setPassword(text)} + value={password} + secureTextEntry={true} + placeholder="Password" + autoCapitalize={'none'} + /> + + + + + ); +} diff --git a/frontend/components/Events/ClubEventCard.js b/frontend/components/Events/ClubEventCard.js new file mode 100644 index 0000000..98954f4 --- /dev/null +++ b/frontend/components/Events/ClubEventCard.js @@ -0,0 +1,16 @@ +import { Button, Text, View } from 'react-native'; + +export function ClubEventCard({ data }) { + const { title, description } = data; + return ( + + {data.title} + {data.description} + {data.attendeesCount} Attendees + {/* More detailed info */} +