diff --git a/.vscode/settings.json b/.vscode/settings.json index 246b0419d0..c001ea6561 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,11 @@ { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "workbench.editorAssociations": { - "*.md": "vscode.markdown.preview.editor" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - } + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "workbench.editorAssociations": { + "*.md": "vscode.markdown.preview.editor" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "githubPullRequests.ignoredPullRequestBranches": ["main"] } diff --git a/App.jsx b/App.jsx new file mode 100644 index 0000000000..317edaaa97 --- /dev/null +++ b/App.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { Landing } from "./pages/Landing"; +import { Login } from "./pages/Login"; +import { Signup } from "./pages/Signup"; +import "./styles.css"; + +export default function App() { + return ( + + + } /> + } /> + } /> + + + ); +} \ No newline at end of file diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000000..a8786740da --- /dev/null +++ b/backend/app.py @@ -0,0 +1,67 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS + +app = Flask(__name__) +CORS(app, supports_credentials=True, origins="*") + +# In-memory event storage for demonstration +EVENTS = [ + { + "id": 1, + "title": "Mock Music Festival", + "date": "2025-09-15", + "location": "Central Park", + "description": "A fun outdoor music festival for all ages.", + "rsvp": 42, + "icon": "🎡", + }, + { + "id": 2, + "title": "Tech Conference", + "date": "2025-10-01", + "location": "Convention Center", + "description": "Join the latest in tech and innovation.", + "rsvp": 87, + "icon": "πŸ’»", + }, + { + "id": 3, + "title": "Art Expo", + "date": "2025-11-05", + "location": "Art Gallery", + "description": "Explore modern art from local artists.", + "rsvp": 30, + "icon": "🎨", + }, +] + + +@app.route("/api/events", methods=["GET", "POST"]) +def events(): + if request.method == "POST": + data = request.json + event = { + "id": len(EVENTS) + 1, + "title": data.get("title"), + "date": data.get("date"), + "location": data.get("location"), + "description": data.get("description"), + "icon": data.get("icon", "πŸŽ‰"), + "rsvp": 0, + } + EVENTS.append(event) + return jsonify(event), 201 + return jsonify(EVENTS) + + +@app.route("/api/events//rsvp", methods=["POST"]) +def rsvp(event_id): + for event in EVENTS: + if event["id"] == event_id: + event["rsvp"] += 1 + return jsonify({"message": "RSVP successful", "event": event}) + return jsonify({"error": "Event not found"}), 404 + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=3001, debug=True) diff --git a/background-music.mp3 b/background-music.mp3 new file mode 100644 index 0000000000..fbf403c14b Binary files /dev/null and b/background-music.mp3 differ diff --git a/carousel.js b/carousel.js new file mode 100644 index 0000000000..1d751ae0fd --- /dev/null +++ b/carousel.js @@ -0,0 +1,27 @@ +const events = [ + { icon: "🎡", title: "Concert Night", date: "Sep 10, 2025", rsvp: 42 }, + { icon: "🍴", title: "Foodie Meetup", date: "Sep 15, 2025", rsvp: 28 }, + { icon: "πŸ’»", title: "Tech Hackathon", date: "Sep 20, 2025", rsvp: 65 }, +]; + +let current = 0; + +function updateCarousel() { + const card = document.getElementById("carousel-card"); + card.innerHTML = ` + ${events[current].icon} +

${events[current].title}

+

${events[current].date} β€’ ${events[current].rsvp} RSVPs

+ `; +} + +document.getElementById("carousel-prev").onclick = function () { + current = (current - 1 + events.length) % events.length; + updateCarousel(); +}; +document.getElementById("carousel-next").onclick = function () { + current = (current + 1) % events.length; + updateCarousel(); +}; + +updateCarousel(); diff --git a/index.html b/index.html index 27a99f796e..ac02a0f562 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,31 @@ - + - - + + - Hello Rigo + E-Venture
- + - \ No newline at end of file + diff --git a/migrations/versions/11c0ae10bf26_.py b/migrations/versions/11c0ae10bf26_.py new file mode 100644 index 0000000000..17dc39d025 --- /dev/null +++ b/migrations/versions/11c0ae10bf26_.py @@ -0,0 +1,48 @@ +"""empty message + +Revision ID: 11c0ae10bf26 +Revises: dd5c9565f9d5 +Create Date: 2025-09-03 18:21:48.921314 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '11c0ae10bf26' +down_revision = 'dd5c9565f9d5' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('is_verified', sa.Boolean(), nullable=True)) + batch_op.alter_column('first_name', + existing_type=sa.VARCHAR(length=80), + type_=sa.String(length=120), + existing_nullable=True) + batch_op.alter_column('last_name', + existing_type=sa.VARCHAR(length=80), + type_=sa.String(length=120), + existing_nullable=True) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.alter_column('last_name', + existing_type=sa.String(length=120), + type_=sa.VARCHAR(length=80), + existing_nullable=True) + batch_op.alter_column('first_name', + existing_type=sa.String(length=120), + type_=sa.VARCHAR(length=80), + existing_nullable=True) + batch_op.drop_column('is_verified') + + # ### end Alembic commands ### diff --git a/migrations/versions/c30bd45045f9_.py b/migrations/versions/c30bd45045f9_.py new file mode 100644 index 0000000000..cb434f3eb4 --- /dev/null +++ b/migrations/versions/c30bd45045f9_.py @@ -0,0 +1,88 @@ +"""empty message + +Revision ID: c30bd45045f9 +Revises: 0763d677d453 +Create Date: 2025-09-03 19:43:57.471352 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c30bd45045f9' +down_revision = '0763d677d453' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('event', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=120), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('location', sa.String(length=120), nullable=False), + sa.Column('date', sa.Date(), nullable=False), + sa.Column('time', sa.Time(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('favorite', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('event_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('favorite_member', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('member_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['member_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('rsvp', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('event_id', sa.Integer(), nullable=False), + sa.Column('response', sa.String(length=10), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('first_name', sa.String(length=120), nullable=True)) + batch_op.add_column(sa.Column('last_name', sa.String(length=120), nullable=True)) + batch_op.add_column(sa.Column('is_verified', sa.Boolean(), nullable=True)) + batch_op.add_column(sa.Column('profile_photo', sa.String(length=255), nullable=True)) + batch_op.add_column(sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True)) + batch_op.add_column(sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_column('updated_at') + batch_op.drop_column('created_at') + batch_op.drop_column('profile_photo') + batch_op.drop_column('is_verified') + batch_op.drop_column('last_name') + batch_op.drop_column('first_name') + + op.drop_table('rsvp') + op.drop_table('favorite_member') + op.drop_table('favorite') + op.drop_table('event') + # ### end Alembic commands ### diff --git a/migrations/versions/dd5c9565f9d5_.py b/migrations/versions/dd5c9565f9d5_.py new file mode 100644 index 0000000000..9061463d91 --- /dev/null +++ b/migrations/versions/dd5c9565f9d5_.py @@ -0,0 +1,86 @@ +"""empty message + +Revision ID: dd5c9565f9d5 +Revises: 0763d677d453 +Create Date: 2025-09-03 17:52:43.508072 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'dd5c9565f9d5' +down_revision = '0763d677d453' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('event', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=120), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('location', sa.String(length=120), nullable=False), + sa.Column('date', sa.Date(), nullable=False), + sa.Column('time', sa.Time(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('favorite', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('event_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('favorite_member', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('member_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['member_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('rsvp', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('event_id', sa.Integer(), nullable=False), + sa.Column('response', sa.String(length=10), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('first_name', sa.String(length=80), nullable=True)) + batch_op.add_column(sa.Column('last_name', sa.String(length=80), nullable=True)) + batch_op.add_column(sa.Column('profile_photo', sa.String(length=255), nullable=True)) + batch_op.add_column(sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True)) + batch_op.add_column(sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_column('updated_at') + batch_op.drop_column('created_at') + batch_op.drop_column('profile_photo') + batch_op.drop_column('last_name') + batch_op.drop_column('first_name') + + op.drop_table('rsvp') + op.drop_table('favorite_member') + op.drop_table('favorite') + op.drop_table('event') + # ### end Alembic commands ### diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..2e63d8556e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,14 @@ "version": "1.0.1", "license": "ISC", "dependencies": { + "@fortawesome/free-solid-svg-icons": "^7.0.1", + "@fortawesome/react-fontawesome": "^3.0.2", + "framer-motion": "^12.23.20", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-icons": "^5.5.0", + "react-router-dom": "^6.30.1" }, "devDependencies": { "@types/react": "^18.2.18", @@ -804,6 +808,53 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.1.tgz", + "integrity": "sha512-0VpNtO5cNe1/HQWMkl4OdncYK/mv9hnBte0Ew0n6DMzmo3Q3WzDFABHm6LeNTipt5zAyhQ6Ugjiu8aLaEjh1gg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.0.1.tgz", + "integrity": "sha512-x0cR55ILVqFpUioSMf6ebpRCMXMcheGN743P05W2RB5uCNpJUqWIqW66Lap8PfL/lngvjTbZj0BNSUweIr/fHQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.0.1.tgz", + "integrity": "sha512-esKuSrl1WMOTMDLNt38i16VfLe/gRZt2ZAJ3Yw7slfs7sj583MKqNFqO57zmhknk1Sya6f9Wys89aCzIJkcqlg==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.0.2.tgz", + "integrity": "sha512-cmp/nT0pPC7HUALF8uc3+D5ECwEBWxYQbOIHwtGUWEu72sWtZc26k5onr920HWOViF0nYaC+Qzz6Ln56SQcaVg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~6 || ~7", + "react": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -944,9 +995,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", - "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -2287,6 +2338,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.23.20", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.20.tgz", + "integrity": "sha512-+GWIKV6Rb8XtZO3gQ1adkZI3Oq14zQdeZdgcy+smOIbXGnqzOcNz1luU27bCtBiUxTXsE9hCLLnbeRJrcbAuLQ==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.20", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3153,6 +3231,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.23.20", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.20.tgz", + "integrity": "sha512-0eFXthY9L6F1trIu/9q99DkBfpAjrdqsw+pZQVEEl6IvS4QiF7Nz9HSbQ5HrXVdE0Xsm72ZDmmoGAivATxEJqg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3506,6 +3599,15 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -3522,12 +3624,12 @@ } }, "node_modules/react-router": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.29.0.tgz", - "integrity": "sha512-DXZJoE0q+KyeVw75Ck6GkPxFak63C4fGqZGNijnWgzB/HzSP1ZfTlBj5COaGWwhrMQ/R8bXiq5Ooy4KG+ReyjQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.22.0" + "@remix-run/router": "1.23.0" }, "engines": { "node": ">=14.0.0" @@ -3537,13 +3639,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.29.0.tgz", - "integrity": "sha512-pkEbJPATRJ2iotK+wUwHfy0xs2T59YPEN8BQxVCPeBZvK7kfPESRc/nyxzdcxR17hXgUPYx2whMwl+eo9cUdnQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.22.0", - "react-router": "6.29.0" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" }, "engines": { "node": ">=14.0.0" @@ -4109,6 +4211,12 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4898,6 +5006,34 @@ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true }, + "@fortawesome/fontawesome-common-types": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.1.tgz", + "integrity": "sha512-0VpNtO5cNe1/HQWMkl4OdncYK/mv9hnBte0Ew0n6DMzmo3Q3WzDFABHm6LeNTipt5zAyhQ6Ugjiu8aLaEjh1gg==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.0.1.tgz", + "integrity": "sha512-x0cR55ILVqFpUioSMf6ebpRCMXMcheGN743P05W2RB5uCNpJUqWIqW66Lap8PfL/lngvjTbZj0BNSUweIr/fHQ==", + "peer": true, + "requires": { + "@fortawesome/fontawesome-common-types": "7.0.1" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.0.1.tgz", + "integrity": "sha512-esKuSrl1WMOTMDLNt38i16VfLe/gRZt2ZAJ3Yw7slfs7sj583MKqNFqO57zmhknk1Sya6f9Wys89aCzIJkcqlg==", + "requires": { + "@fortawesome/fontawesome-common-types": "7.0.1" + } + }, + "@fortawesome/react-fontawesome": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.0.2.tgz", + "integrity": "sha512-cmp/nT0pPC7HUALF8uc3+D5ECwEBWxYQbOIHwtGUWEu72sWtZc26k5onr920HWOViF0nYaC+Qzz6Ln56SQcaVg==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -4999,9 +5135,9 @@ } }, "@remix-run/router": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", - "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==" + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==" }, "@types/babel__core": { "version": "7.20.5", @@ -5941,6 +6077,16 @@ "is-callable": "^1.2.7" } }, + "framer-motion": { + "version": "12.23.20", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.20.tgz", + "integrity": "sha512-+GWIKV6Rb8XtZO3gQ1adkZI3Oq14zQdeZdgcy+smOIbXGnqzOcNz1luU27bCtBiUxTXsE9hCLLnbeRJrcbAuLQ==", + "requires": { + "motion-dom": "^12.23.20", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6490,6 +6636,19 @@ "brace-expansion": "^1.1.7" } }, + "motion-dom": { + "version": "12.23.20", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.20.tgz", + "integrity": "sha512-0eFXthY9L6F1trIu/9q99DkBfpAjrdqsw+pZQVEEl6IvS4QiF7Nz9HSbQ5HrXVdE0Xsm72ZDmmoGAivATxEJqg==", + "requires": { + "motion-utils": "^12.23.6" + } + }, + "motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6715,6 +6874,12 @@ "scheduler": "^0.23.2" } }, + "react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "requires": {} + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6727,20 +6892,20 @@ "dev": true }, "react-router": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.29.0.tgz", - "integrity": "sha512-DXZJoE0q+KyeVw75Ck6GkPxFak63C4fGqZGNijnWgzB/HzSP1ZfTlBj5COaGWwhrMQ/R8bXiq5Ooy4KG+ReyjQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "requires": { - "@remix-run/router": "1.22.0" + "@remix-run/router": "1.23.0" } }, "react-router-dom": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.29.0.tgz", - "integrity": "sha512-pkEbJPATRJ2iotK+wUwHfy0xs2T59YPEN8BQxVCPeBZvK7kfPESRc/nyxzdcxR17hXgUPYx2whMwl+eo9cUdnQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "requires": { - "@remix-run/router": "1.22.0", - "react-router": "6.29.0" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" } }, "reflect.getprototypeof": { @@ -7124,6 +7289,11 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 0caab10749..6157e82a65 100755 --- a/package.json +++ b/package.json @@ -8,10 +8,10 @@ "main": "index.js", "scripts": { "dev": "vite", - "start": "vite", - "build": "vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "start": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" }, "author": { "name": "Alejandro Sanchez", @@ -30,13 +30,13 @@ "license": "ISC", "devDependencies": { "@types/react": "^18.2.18", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.4", - "eslint": "^8.46.0", - "eslint-plugin-react": "^7.33.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.8" + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.0.4", + "eslint": "^8.46.0", + "eslint-plugin-react": "^7.33.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "vite": "^4.4.8" }, "babel": { "presets": [ @@ -54,9 +54,13 @@ ] }, "dependencies": { + "@fortawesome/free-solid-svg-icons": "^7.0.1", + "@fortawesome/react-fontawesome": "^3.0.2", + "framer-motion": "^12.23.20", "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-icons": "^5.5.0", + "react-router-dom": "^6.30.1" } } diff --git a/src/api/admin.py b/src/api/admin.py index 3eecb64140..c1a6c6d029 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,7 +1,7 @@ import os from flask_admin import Admin -from .models import db, User +from .models import db, User, Event from flask_admin.contrib.sqla import ModelView def setup_admin(app): @@ -14,4 +14,5 @@ def setup_admin(app): admin.add_view(ModelView(User, db.session)) # You can duplicate that line to add mew models - # admin.add_view(ModelView(YourModelName, db.session)) \ No newline at end of file + # admin.add_view(ModelView(YourModelName, db.session)) + admin.add_view(ModelView(Event, db.session)) \ No newline at end of file diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..28ae46fd82 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,104 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String, Boolean, Text, Date, Time, DateTime, func db = SQLAlchemy() -class User(db.Model): - id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False) + password = db.Column(db.String(120), nullable=False) + first_name = db.Column(db.String(120)) + last_name = db.Column(db.String(120)) + is_verified = db.Column(db.Boolean, default=False) # <-- Add this line + is_active = db.Column(Boolean(), nullable=False, default=True) + profile_photo = db.Column(db.String(255), nullable=True) + created_at = db.Column(DateTime(timezone=True), server_default=func.now()) + updated_at = db.Column(DateTime(timezone=True), onupdate=func.now()) def serialize(self): return { "id": self.id, "email": self.email, - # do not serialize the password, its a security breach - } \ No newline at end of file + "first_name": self.first_name, + "last_name": self.last_name, + "profile_photo": self.profile_photo, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + +class Event(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(120), nullable=False) + description = db.Column(Text, nullable=True) + location = db.Column(db.String(120), nullable=False) + date = db.Column(Date, nullable=False) + time = db.Column(Time, nullable=False) + created_at = db.Column(DateTime(timezone=True), server_default=func.now()) + updated_at = db.Column(DateTime(timezone=True), onupdate=func.now()) + + def serialize(self): + return { + "id": self.id, + "title": self.title, + "description": self.description, + "location": self.location, + "date": self.date.isoformat() if self.date else None, + "time": self.time.isoformat() if self.time else None, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + +class Favorite(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) + created_at = db.Column(DateTime(timezone=True), server_default=func.now()) + updated_at = db.Column(DateTime(timezone=True), onupdate=func.now()) + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "event_id": self.event_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + +class RSVP(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) + response = db.Column(db.String(10), nullable=False) # "yes", "no", "maybe" + created_at = db.Column(DateTime(timezone=True), server_default=func.now()) + updated_at = db.Column(DateTime(timezone=True), onupdate=func.now()) + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "event_id": self.event_id, + "response": self.response, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + +class FavoriteMember(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + member_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_at = db.Column(DateTime(timezone=True), server_default=func.now()) + updated_at = db.Column(DateTime(timezone=True), onupdate=func.now()) + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "member_id": self.member_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..15882a7ebc 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,22 +1,388 @@ -""" -This module takes care of starting the API Server, Loading the DB and Adding the endpoints -""" -from flask import Flask, request, jsonify, url_for, Blueprint -from api.models import db, User -from api.utils import generate_sitemap, APIException +import time +import re +from werkzeug.security import generate_password_hash, check_password_hash +from flask_jwt_extended import ( + create_access_token, jwt_required, get_jwt_identity, + get_jwt, unset_jwt_cookies +) +from api.models import db, User, Favorite, Event, FavoriteMember, RSVP +from flask import request, jsonify, Blueprint from flask_cors import CORS api = Blueprint('api', __name__) +# 'api' is your Blueprint instance +CORS( + api, + origins=[ + "https://friendly-computing-machine-pxw4p4r46rq2r7gp-3001.app.github.dev", + "http://localhost:3000", + "http://localhost:3001" + ], + supports_credentials=True, + allow_headers="*", + methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + expose_headers="*" +) +api = Blueprint('api', __name__) + +login_attempts = {} +jwt_blacklist = set() + +# Logout Endpoint + + +@api.route('/logout', methods=['POST']) +@jwt_required() +def logout(): + jti = get_jwt()["jti"] + jwt_blacklist.add(jti) + response = jsonify(msg="Logout successful") + unset_jwt_cookies(response) + return response, 200 + + +@api.after_request +def add_cors_headers(response): + response.headers.add('Access-Control-Allow-Origin', '*') + response.headers.add('Access-Control-Allow-Credentials', 'true') + response.headers.add('Access-Control-Allow-Headers', + 'Content-Type,Authorization') + response.headers.add('Access-Control-Allow-Methods', + 'GET,POST,PUT,DELETE,OPTIONS') + return response + +# JWT blacklist check (add to your JWT setup in app.py) +# from flask_jwt_extended import JWTManager +# jwt = JWTManager(app) +# @jwt.token_in_blocklist_loader +# def check_if_token_revoked(jwt_header, jwt_payload): +# return jwt_payload["jti"] in jwt_blacklist + +# User Profile Endpoint + + +@api.route('/profile', methods=['GET']) +@jwt_required() +def get_profile(): + user_id = get_jwt_identity() + user = User.query.get(user_id) + if not user: + return jsonify(msg="User not found"), 404 + return jsonify({ + "email": user.email, + "first_name": user.first_name, + "last_name": user.last_name + }), 200 + + +@api.route('/profile', methods=['PUT']) +@jwt_required() +def update_profile(): + user_id = get_jwt_identity() + user = User.query.get(user_id) + data = request.get_json() + user.first_name = data.get("first_name", user.first_name) + user.last_name = data.get("last_name", user.last_name) + user.email = data.get("email", user.email) + user.location = data.get("location", user.location) + user.language = data.get("language", user.language) + db.session.commit() + return jsonify(msg="Profile updated"), 200 + +# Password Reset (request and reset) + + +@api.route('/password-reset/request', methods=['POST']) +def request_password_reset(): + data = request.get_json() + email = data.get("email") + user = User.query.filter_by(email=email).first() + if not user: + return jsonify(msg="User not found"), 404 + # Generate token and send email (pseudo-code) + reset_token = create_access_token(identity=user.id, expires_delta=False) + # send_email(user.email, reset_token) # Implement this + return jsonify(msg="Password reset email sent"), 200 + + +@api.route('/password-reset/confirm', methods=['POST']) +def confirm_password_reset(): + data = request.get_json() + token = data.get("token") + new_password = data.get("new_password") + # Decode token and reset password + from flask_jwt_extended import decode_token + try: + identity = decode_token(token)["sub"] + user = User.query.get(identity) + user.password = generate_password_hash(new_password) + db.session.commit() + return jsonify(msg="Password updated"), 200 + except Exception: + return jsonify(msg="Invalid token"), 400 + +# Login Attempt Limiting (complete logic) + + +@api.route('/login', methods=['POST']) +def login(): + data = request.get_json() + email = data.get("email") + password = data.get("password") + identifier = email + now = time.time() + attempts = login_attempts.get(identifier, {"count": 0, "last": now}) + + if attempts["count"] >= 5 and now - attempts["last"] < 300: + return jsonify(msg="Too many login attempts. Try again later."), 429 + + user = User.query.filter_by(email=email).first() + if not user or not check_password_hash(user.password, password): + attempts["count"] += 1 + attempts["last"] = now + login_attempts[identifier] = attempts + return jsonify(msg="Invalid credentials"), 401 + + # if not user.is_verified: + # return jsonify(msg="Email not verified"), 403 + + login_attempts[identifier] = {"count": 0, "last": now} + access_token = create_access_token(identity=user.id) + return jsonify(access_token=access_token), 200 + +# Email Verification (pseudo-code) + + +@api.route('/signup', methods=['POST']) +def signup(): + data = request.get_json() + email = data.get("email") + password = data.get("password") + first_name = data.get("first_name") + last_name = data.get("last_name") + + if not email or not password: + return jsonify(msg="Email and password required"), 400 + if not re.match(r"[^@]+@[^@]+\.[^@]+", email): + return jsonify(msg="Invalid email format"), 400 + if User.query.filter_by(email=email).first(): + return jsonify(msg="Email already registered"), 409 + if len(password) < 8 or not any(c.isdigit() for c in password) or not any(c.isupper() for c in password): + return jsonify(msg="Password must be at least 8 chars, include a digit and uppercase"), 400 + + hashed_pw = generate_password_hash(password) + user = User(email=email, password=hashed_pw, + first_name=first_name, last_name=last_name) + db.session.add(user) + db.session.commit() + return jsonify(msg="Signup successful"), 201 + + +# Event CRUD + +@api.route('/my-events', methods=['GET']) +@jwt_required() +def my_events(): + user_id = get_jwt_identity() + # Events created by user (if Event has a creator field) + created_events = [] + if hasattr(Event, 'creator_id'): + created_events = Event.query.filter_by(creator_id=user_id).all() + # Events RSVP'd by user + rsvp_event_ids = [ + rsvp.event_id for rsvp in RSVP.query.filter_by(user_id=user_id).all()] + rsvp_events = Event.query.filter(Event.id.in_( + rsvp_event_ids)).all() if rsvp_event_ids else [] + # Combine and deduplicate + all_events = {event.id: event for event in created_events + rsvp_events} + return jsonify([event.serialize() for event in all_events.values()]), 200 + + +@api.route('/events', methods=['POST']) +@jwt_required() +def create_event(): + data = request.get_json() + title = data.get('title') + description = data.get('description') + location = data.get('location') + date = data.get('date') + time_ = data.get('time') + if not title or not location or not date or not time_: + return jsonify({"error": "Missing fields"}), 400 + event = Event(title=title, description=description, + location=location, date=date, time=time_) + db.session.add(event) + db.session.commit() + return jsonify(event.serialize()), 201 + + +@api.route('/events', methods=['GET']) +def list_events(): + events = Event.query.all() + return jsonify([event.serialize() for event in events]), 200 + + +@api.route('/events/', methods=['GET']) +def get_event(event_id): + event = Event.query.get(event_id) + if not event: + return jsonify({"error": "Event not found"}), 404 + return jsonify(event.serialize()), 200 + + +@api.route('/events/', methods=['PUT']) +@jwt_required() +def update_event(event_id): + event = Event.query.get(event_id) + if not event: + return jsonify({"error": "Event not found"}), 404 + data = request.get_json() + event.title = data.get('title', event.title) + event.description = data.get('description', event.description) + event.location = data.get('location', event.location) + event.date = data.get('date', event.date) + event.time = data.get('time', event.time) + db.session.commit() + return jsonify(event.serialize()), 200 + + +@api.route('/events/', methods=['DELETE']) +@jwt_required() +def delete_event(event_id): + event = Event.query.get(event_id) + if not event: + return jsonify({"error": "Event not found"}), 404 + db.session.delete(event) + db.session.commit() + return jsonify({"msg": "Event deleted"}), 200 + +# RSVP + + +@api.route('/my-rsvps', methods=['GET']) +@jwt_required() +def my_rsvps(): + user_id = get_jwt_identity() + rsvps = RSVP.query.filter_by(user_id=user_id).all() + result = [] + for rsvp in rsvps: + event = Event.query.get(rsvp.event_id) + result.append({ + "event_id": rsvp.event_id, + "event_title": event.title if event else None, + "event_date": event.date.isoformat() if event and event.date else None, + "event_location": event.location if event else None, + "status": rsvp.response + }) + return jsonify(result), 200 + + +@api.route('/events//rsvp', methods=['POST']) +@jwt_required() +def rsvp_event(event_id): + user_id = get_jwt_identity() + response = request.json.get('response') + if response not in ["yes", "no", "maybe"]: + return jsonify({"error": "Invalid response"}), 400 + event = Event.query.get(event_id) + if not event: + return jsonify({"error": "Event not found"}), 404 + rsvp = RSVP.query.filter_by(user_id=user_id, event_id=event_id).first() + if rsvp: + rsvp.response = response + else: + rsvp = RSVP(user_id=user_id, event_id=event_id, response=response) + db.session.add(rsvp) + db.session.commit() + return jsonify(rsvp.serialize()), 200 + + +@api.route('/events//rsvp', methods=['GET']) +@jwt_required() +def get_event_rsvps(event_id): + rsvps = RSVP.query.filter_by(event_id=event_id).all() + return jsonify([rsvp.serialize() for rsvp in rsvps]), 200 + +# Favorites + + +@api.route('/favorites', methods=['POST']) +@jwt_required() +def add_favorite(): + user_id = get_jwt_identity() + event_id = request.json.get('event_id') + if not event_id or not Event.query.get(event_id): + return jsonify({"error": "Event not found"}), 404 + if Favorite.query.filter_by(user_id=user_id, event_id=event_id).first(): + return jsonify({"msg": "Already favorited"}), 400 + favorite = Favorite(user_id=user_id, event_id=event_id) + db.session.add(favorite) + db.session.commit() + return jsonify({"msg": "Event favorited", "favorite": favorite.serialize()}), 201 + + +@api.route('/favorites', methods=['GET']) +@jwt_required() +def list_favorites(): + user_id = get_jwt_identity() + favorites = Favorite.query.filter_by(user_id=user_id).all() + return jsonify([fav.serialize() for fav in favorites]), 200 + + +@api.route('/favorites/', methods=['DELETE']) +@jwt_required() +def remove_favorite(event_id): + user_id = get_jwt_identity() + favorite = Favorite.query.filter_by( + user_id=user_id, event_id=event_id).first() + if not favorite: + return jsonify({"error": "Favorite not found"}), 404 + db.session.delete(favorite) + db.session.commit() + return jsonify({"msg": "Favorite removed"}), 200 + +# Favorite Members + + +@api.route('/favorite-members', methods=['POST']) +@jwt_required() +def add_favorite_member(): + user_id = get_jwt_identity() + member_id = request.json.get('member_id') + if not member_id or not User.query.get(member_id): + return jsonify({"error": "Member not found"}), 404 + if FavoriteMember.query.filter_by(user_id=user_id, member_id=member_id).first(): + return jsonify({"msg": "Already favorited"}), 400 + favorite = FavoriteMember(user_id=user_id, member_id=member_id) + db.session.add(favorite) + db.session.commit() + return jsonify({"msg": "Member favorited", "favorite": favorite.serialize()}), 201 + + +@api.route('/favorite-members', methods=['GET']) +@jwt_required() +def list_favorite_members(): + user_id = get_jwt_identity() + favorites = FavoriteMember.query.filter_by(user_id=user_id).all() + return jsonify([fav.serialize() for fav in favorites]), 200 + -# Allow CORS requests to this API -CORS(api) +@api.route('/favorite-members/', methods=['DELETE']) +@jwt_required() +def remove_favorite_member(member_id): + user_id = get_jwt_identity() + favorite = FavoriteMember.query.filter_by( + user_id=user_id, member_id=member_id).first() + if not favorite: + return jsonify({"error": "Favorite not found"}), 404 + db.session.delete(favorite) + db.session.commit() + return jsonify({"msg": "Favorite removed"}), 200 -@api.route('/hello', methods=['POST', 'GET']) -def handle_hello(): +def send_email(recipient, token): + print(f"Send password reset email to {recipient} with token: {token}") - response_body = { - "message": "Hello! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request" - } - return jsonify(response_body), 200 +def send_verification_email(recipient, token): + print(f"Send verification email to {recipient} with token: {token}") diff --git a/src/app.py b/src/app.py index 1b3340c0fa..a35731c87b 100644 --- a/src/app.py +++ b/src/app.py @@ -1,15 +1,14 @@ -""" -This module takes care of starting the API Server, Loading the DB and Adding the endpoints -""" import os from flask import Flask, request, jsonify, url_for, send_from_directory from flask_migrate import Migrate from flask_swagger import swagger +from flask_jwt_extended import JWTManager from api.utils import APIException, generate_sitemap from api.models import db -from api.routes import api +from api.routes import api, jwt_blacklist # Import from routes.py from api.admin import setup_admin from api.commands import setup_commands +from flask_cors import CORS # from models import Person @@ -17,6 +16,11 @@ static_file_dir = os.path.join(os.path.dirname( os.path.realpath(__file__)), '../dist/') app = Flask(__name__) +CORS(app, origins=[ + "https://friendly-computing-machine-pxw4p4r46rq2r7gp-3000.app.github.dev", + "https://upgraded-system-7vgj4vjj6j52rx7j-3000.app.github.dev", + "https://jubilant-telegram-9759j7q57vg92pgg4-3000.app.github.dev", +], supports_credentials=True) app.url_map.strict_slashes = False # database condiguration @@ -57,6 +61,8 @@ def sitemap(): return send_from_directory(static_file_dir, 'index.html') # any other endpoint will try to serve it like a static file + + @app.route('/', methods=['GET']) def serve_any_other_file(path): if not os.path.isfile(os.path.join(static_file_dir, path)): @@ -66,7 +72,19 @@ def serve_any_other_file(path): return response +app.config["JWT_TOKEN_LOCATION"] = ["headers"] +app.config["JWT_HEADER_NAME"] = "Authorization" +app.config["JWT_HEADER_TYPE"] = "Bearer" + +jwt = JWTManager(app) + + +@jwt.token_in_blocklist_loader +def check_if_token_revoked(jwt_header, jwt_payload): + return jwt_payload["jti"] in jwt_blacklist + + # this only runs if `$ python src/main.py` is executed if __name__ == '__main__': PORT = int(os.environ.get('PORT', 3001)) - app.run(host='0.0.0.0', port=PORT, debug=True) + app.run(host='0.0.0.0', port=PORT, debug=True) \ No newline at end of file diff --git a/src/components/LandingPage.jsx b/src/components/LandingPage.jsx new file mode 100644 index 0000000000..47c0b72ccc --- /dev/null +++ b/src/components/LandingPage.jsx @@ -0,0 +1,119 @@ +import React from "react"; +import "./LandingPage.css"; + +const features = [ + { icon: "πŸ“…", title: "My Events", desc: "Create, view, and manage events." }, + { icon: "πŸ—³οΈ", title: "RSVP & Polling", desc: "Let guests RSVP and vote on event options." }, + { icon: "⭐", title: "Favorites", desc: "Save your favorite events and people." }, + { icon: "πŸ””", title: "Notifications", desc: "Stay updated with event changes." }, + { icon: "πŸ‘€", title: "Profile", desc: "Customize your experience." }, +]; + +const upcomingEvents = [ + { icon: "🎡", title: "Concert Night", date: "Sep 10, 2025", rsvp: 42 }, + { icon: "🍴", title: "Foodie Meetup", date: "Sep 15, 2025", rsvp: 28 }, + { icon: "πŸ’»", title: "Tech Hackathon", date: "Sep 20, 2025", rsvp: 65 }, +]; + +const whyPanels = [ + { icon: "⚑", title: "Simple & Fast", desc: "Easy event planning." }, + { icon: "🌐", title: "Stay Connected", desc: "RSVP, follow, and interact." }, + { icon: "πŸ“Έ", title: "Memories That Last", desc: "Capture and revisit your events." }, +]; + +export default function LandingPage() { + return ( +
+ {/* Navbar */} + + + {/* Hero Section */} +
+ +
+

Where Every Event Becomes a Story

+

Plan, share, and remember your events.

+ +
+
+ + {/* Features Section */} +
+

Features

+
+ {features.map((f, i) => ( +
+ {f.icon} +

{f.title}

+

{f.desc}

+
+ ))} +
+
+ + {/* Upcoming Events Carousel */} +
+

Upcoming Events

+
+ {upcomingEvents.map((e, i) => ( +
+ {e.icon} +

{e.title}

+

{e.date} β€’ {e.rsvp} RSVPs

+
+ ))} +
+
+ + {/* Why E-Venture Section */} +
+

Why E-Venture?

+
+ {whyPanels.map((p, i) => ( +
+ {p.icon} +

{p.title}

+

{p.desc}

+
+ ))} +
+
+ + {/* CTA Section */} +
+

Ready to start your next adventure?

+ Sign Up Free +
+ + {/* Footer */} + +
+ ); +} \ No newline at end of file diff --git a/src/front/App.jsx b/src/front/App.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/front/components/DashboardNavbar.jsx b/src/front/components/DashboardNavbar.jsx new file mode 100644 index 0000000000..4b5fdc9409 --- /dev/null +++ b/src/front/components/DashboardNavbar.jsx @@ -0,0 +1,19 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import "../pages/Dashboard.css"; + +const DashboardNavbar = () => ( + +); + +export default DashboardNavbar; diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..d04e323c89 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -4,16 +4,8 @@ export const Navbar = () => { return ( ); }; \ No newline at end of file diff --git a/src/front/pages/CreateEvent.css b/src/front/pages/CreateEvent.css new file mode 100644 index 0000000000..2eeeed6864 --- /dev/null +++ b/src/front/pages/CreateEvent.css @@ -0,0 +1,89 @@ +@media (max-width: 768px) { + .photo-upload-wrapper { + width: 100%; + aspect-ratio: 5 / 2; + margin-bottom: 10px; + } + .photo-preview { + width: 100%; + height: auto; + margin-bottom: 8px; + } + .photo-preview-container { + flex-direction: column; + gap: 4px; + width: 100%; + } + .file-upload-label { + font-size: 24px; + width: 40px; + height: 40px; + } +} +.photo-upload-wrapper { + position: relative; + width: 300px; + aspect-ratio: 5 / 2; + overflow: hidden; + margin-bottom: 15px; + border-radius: 8px; +} + +.file-upload-input { + display: none; +} + +.file-upload-label { + display: flex; + align-items: center; + justify-content: center; + color: #ff2d75; + font-size: 32px; + width: 50px; + height: 100px; + border-radius: 50%; + cursor: pointer; + user-select: none; + margin: 0; + padding: 0; + box-sizing: border-box; + line-height: 1; +} + +.plus-sign { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + line-height: 1; +} + +.photo-preview { + width: 200px; + height: 100%; + object-fit: cover; + object-position: center; + border-radius: 8px; + cursor: pointer; + margin-bottom: 10px; +} + +.photo-preview-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; + width: 100%; +} + +.change-photo-label { + font-size: 12px; + color: #ff2d75; + cursor: pointer; + user-select: none; + text-decoration: underline; + white-space: nowrap; +} \ No newline at end of file diff --git a/src/front/pages/CreateEvent.jsx b/src/front/pages/CreateEvent.jsx new file mode 100644 index 0000000000..91b4f41841 --- /dev/null +++ b/src/front/pages/CreateEvent.jsx @@ -0,0 +1,112 @@ +import React, { useState } from 'react'; +import "./Landing.css"; +import "./CreateEvent.css"; + +function CreateEvent() { + const [eventName, setEventName] = useState(''); + const [eventDate, setEventDate] = useState(''); + const [eventLocation, setEventLocation] = useState(''); + const [eventTime, setEventTime] = useState(''); + const [eventDescription, setEventDescription] = useState(''); + const [eventPhoto, setEventPhoto] = useState(null); + const [message, setMessage] = useState(''); + + function handlePhotoChange(e) { + setEventPhoto(e.target.files[0]); + } + + function handleSubmit(e) { + e.preventDefault(); + if (eventName && eventDate && eventLocation && eventTime && eventDescription && eventPhoto) { + setMessage("Event created!"); + setEventName(''); + setEventDate(''); + setEventLocation(''); + setEventTime(''); + setEventDescription(''); + setEventPhoto(null); + e.target.reset(); + } else { + setMessage("Please complete all fields."); + } + } + + return ( +
+

Create Event

+
+
+ setEventName(e.target.value)} + className="signup-input" + /> + setEventDate(e.target.value)} + className="signup-input" + /> + setEventLocation(e.target.value)} + className="signup-input" + /> + setEventTime(e.target.value)} + className="signup-input" + /> +