diff --git a/templates/app/app-vite/package-lock.json b/templates/app/app-vite/package-lock.json index aea80d61..9cf4512a 100644 --- a/templates/app/app-vite/package-lock.json +++ b/templates/app/app-vite/package-lock.json @@ -22,7 +22,7 @@ "json-server": "^0.17.4", "json-server-auth": "^2.1.0", "nodemon": "^3.1.0", - "prettier": "^3.2.5", + "prettier": "^3.8.3", "vite": "^5.2.0" }, "engines": { @@ -4991,10 +4991,11 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, diff --git a/templates/app/app-vite/package.json b/templates/app/app-vite/package.json index 1d8bc2b2..3a241c1f 100644 --- a/templates/app/app-vite/package.json +++ b/templates/app/app-vite/package.json @@ -25,13 +25,13 @@ "@eslint/js": "^9.0.0", "@vitejs/plugin-react": "^4.2.1", "concurrently": "^9.1.0", - "nodemon": "^3.1.0", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "globals": "^15.0.0", "json-server": "^0.17.4", "json-server-auth": "^2.1.0", - "prettier": "^3.2.5", + "nodemon": "^3.1.0", + "prettier": "^3.8.3", "vite": "^5.2.0" } } diff --git a/templates/app/app-vite/src/components/EventCard/EventCard.css b/templates/app/app-vite/src/components/EventCard/EventCard.css new file mode 100644 index 00000000..1a350b1f --- /dev/null +++ b/templates/app/app-vite/src/components/EventCard/EventCard.css @@ -0,0 +1,54 @@ +.event-card { + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 20px; + max-width: 420px; + display: flex; + flex-direction: column; + gap: 12px; +} +.event-category { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #666; +} +.event-title { + margin: 0; + font-size: 18px; +} +.event-meta { + display: flex; + flex-direction: column; + gap: 4px; + color: #444; + font-size: 14px; +} +.event-meta p { + margin: 0; +} +.event-footer { + display: flex; + align-items: center; + gap: 12px; + margin-top: 4px; +} +.event-price { + font-weight: 700; + font-size: 16px; +} +.sold-out { + color: #c0392b; + font-size: 13px; +} +.available { + color: #27ae60; + font-size: 13px; +} +.event-link { + margin-left: auto; + font-size: 14px; + color: #0066cc; + text-decoration: none; +} diff --git a/templates/app/app-vite/src/components/EventCard/EventCard.jsx b/templates/app/app-vite/src/components/EventCard/EventCard.jsx new file mode 100644 index 00000000..70dde3ed --- /dev/null +++ b/templates/app/app-vite/src/components/EventCard/EventCard.jsx @@ -0,0 +1,50 @@ +import { Link } from "react-router-dom"; +import "./EventCard.css"; + + +export default function EventCard({ event }) { + const { + id, + name, + date, + time, + venue, + city, + category, + price, + ticketsAvailable, + } = event; + + return ( +
+ {category} + +

{name}

+ +
+

+ 📅 {date} at {time} +

+

+ 📍 {venue}, {city} +

+
+ +
+ + {price === 0 ? "Free" : `€${price}`} + + + {ticketsAvailable === 0 + ? "Sold out" + : `${ticketsAvailable} tickets left`} + + + View details → + +
+
+ ); +} diff --git a/templates/app/app-vite/src/components/EventDetail/EventDetail.css b/templates/app/app-vite/src/components/EventDetail/EventDetail.css new file mode 100644 index 00000000..350c5961 --- /dev/null +++ b/templates/app/app-vite/src/components/EventDetail/EventDetail.css @@ -0,0 +1,62 @@ +.event-detail { + max-width: 680px; + margin: 40px auto; + padding: 0 20px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.back-link { + font-size: 14px; + color: #0066cc; + text-decoration: none; +} + +.event-detail-title { + margin: 0; + font-size: 28px; +} + +.event-detail-meta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + background: #f9f9f9; + border-radius: 8px; + padding: 20px; +} + +.meta-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.meta-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #999; +} + +.meta-value { + font-size: 15px; + color: #222; +} + +.event-description h2 { + font-size: 18px; + margin-bottom: 8px; +} +.event-description p { + line-height: 1.7; + color: #444; +} +.sold-out { + color: #c0392b; +} +.available { + color: #27ae60; +} diff --git a/templates/app/app-vite/src/components/EventDetail/EventDetail.jsx b/templates/app/app-vite/src/components/EventDetail/EventDetail.jsx index 69c89f9b..60b4f18a 100644 --- a/templates/app/app-vite/src/components/EventDetail/EventDetail.jsx +++ b/templates/app/app-vite/src/components/EventDetail/EventDetail.jsx @@ -1,11 +1,58 @@ -// TODO: display at least date, time, venue, city, and description for one event -// TODO: use useParams() to get the event id from the URL -// TODO: fetch the event from GET /events/:id instead of using mock data +import events from "../../data/events.js"; +import { Link } from "react-router-dom"; +import "./EventDetail.css"; export default function EventDetail() { + const event = events[0]; return ( -
-

Event detail — coming soon.

+
+ + ← Back to all events + + + {event.category} + +

{event.name}

+ +
+
+ Date + {event.date} +
+
+ Time + {event.time} +
+
+ Venue + {event.venue} +
+
+ City + {event.city} +
+
+ Price + + {event.price === 0 ? "Free" : `€${event.price}`} + +
+
+ Availability + + {event.ticketsAvailable === 0 + ? "Sold out" + : `${ticketsAvailable} ticket${ticketsAvailable === 1 ? "" : "s"} left`} + +
+
+ +
+

About this event

+

{event.description}

+
); -} \ No newline at end of file +} diff --git a/templates/app/app-vite/src/components/EventList/EventList.css b/templates/app/app-vite/src/components/EventList/EventList.css new file mode 100644 index 00000000..9a07c638 --- /dev/null +++ b/templates/app/app-vite/src/components/EventList/EventList.css @@ -0,0 +1,29 @@ +.event-list-container { + max-width: 1200px; + margin: 0 auto; + padding: 40px 20px; +} + +.event-list-container h1 { + text-align: center; + margin-bottom: 30px; + font-size: 32px; + color: #333; +} + +.event-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 24px; +} + +@media (max-width: 768px) { + .event-list-container { + padding: 20px 16px; + } + + .event-list-container h1 { + font-size: 24px; + margin-bottom: 20px; + } +} diff --git a/templates/app/app-vite/src/components/EventList/EventList.jsx b/templates/app/app-vite/src/components/EventList/EventList.jsx index 0436d091..5c5d8d4b 100644 --- a/templates/app/app-vite/src/components/EventList/EventList.jsx +++ b/templates/app/app-vite/src/components/EventList/EventList.jsx @@ -1,30 +1,16 @@ import events from "../../data/events.js"; - -// TODO: split each event below into its own EventCard component -// TODO: add a "Buy ticket" button to each event card -// TODO: replace the mock data import with a fetch call to GET /events +import EventCard from "../EventCard/EventCard.jsx"; +import "./EventList.css"; export default function EventList() { return ( - +
+

Upcoming Events

+
+ {events.map((event) => ( + + ))} +
+
); } diff --git a/templates/app/app-vite/src/components/HomePage/HomePage.css b/templates/app/app-vite/src/components/HomePage/HomePage.css new file mode 100644 index 00000000..ac402463 --- /dev/null +++ b/templates/app/app-vite/src/components/HomePage/HomePage.css @@ -0,0 +1,52 @@ +.hero { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 80px 24px; + gap: 20px; +} + +.hero-badge { + background: #eef2ff; + color: #3730a3; + font-size: 13px; + font-weight: 600; + padding: 6px 14px; + border-radius: 20px; + letter-spacing: 0.03em; +} + +.hero-title { + font-size: 48px; + font-weight: 700; + line-height: 1.15; + color: #111; + margin: 0; + max-width: 600px; +} + +.hero-subtitle { + font-size: 18px; + color: #666; + max-width: 480px; + line-height: 1.6; + margin: 0; +} + +.hero-cta { + display: inline-block; + background: #3730a3; + color: #fff; + text-decoration: none; + padding: 14px 32px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; + margin-top: 8px; + transition: background 0.15s; +} + +.hero-cta:hover { + background: #312e81; +} diff --git a/templates/app/app-vite/src/components/HomePage/HomePage.jsx b/templates/app/app-vite/src/components/HomePage/HomePage.jsx index 88fa881a..6c2bf790 100644 --- a/templates/app/app-vite/src/components/HomePage/HomePage.jsx +++ b/templates/app/app-vite/src/components/HomePage/HomePage.jsx @@ -1,18 +1,20 @@ -// Feel free to replace the content of this component with your own -function HomePage() { +import { Link } from "react-router-dom"; +import "./HomePage.css"; + +export default function HomePage() { return ( -
- Welcome to the homepage! +
+ 🎟 Events platform +

+ Discover events
happening near you +

+

+ Conferences, workshops, and hackathons — find your next experience and + grab your ticket in seconds. +

+ + Browse events → +
); } - -export default HomePage; diff --git a/templates/app/app-vite/src/components/Layout/Layout.css b/templates/app/app-vite/src/components/Layout/Layout.css new file mode 100644 index 00000000..922419cc --- /dev/null +++ b/templates/app/app-vite/src/components/Layout/Layout.css @@ -0,0 +1,118 @@ +.site-wrapper { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.site-header { + border-bottom: 1px solid #e0e0e0; + background: #fff; + position: sticky; + top: 0; + z-index: 100; +} + +.site-nav { + max-width: 1100px; + margin: 0 auto; + padding: 0 24px; + height: 64px; + display: flex; + align-items: center; + gap: 32px; +} + +.nav-logo { + display: flex; + align-items: center; +} +.nav-logo img { + display: block; +} + +.nav-links { + display: flex; + list-style: none; + margin: 0; + padding: 0; + gap: 4px; + flex: 1; +} + +.nav-link { + padding: 6px 12px; + border-radius: 6px; + text-decoration: none; + color: #444; + font-size: 15px; + transition: background 0.15s; +} + +.nav-link:hover { + background: #f3f3f3; +} +.nav-link.active { + background: #eef2ff; + color: #3730a3; + font-weight: 500; +} + +.nav-auth { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; +} + +.nav-user { + font-size: 14px; + color: #666; +} + +.btn-signout { + padding: 6px 14px; + border: 1px solid #e0e0e0; + border-radius: 6px; + background: #fff; + cursor: pointer; + font-size: 14px; + color: #444; +} + +.btn-login { + padding: 6px 14px; + border-radius: 6px; + text-decoration: none; + font-size: 14px; + color: #444; + border: 1px solid #e0e0e0; +} + +.btn-register { + padding: 6px 14px; + border-radius: 6px; + text-decoration: none; + font-size: 14px; + color: #fff; + background: #3730a3; +} + +.site-main { + flex: 1; + max-width: 1100px; + margin: 0 auto; + padding: 32px 24px; + width: 100%; +} + +.site-footer { + border-top: 1px solid #eee; + padding: 20px 24px; + text-align: center; + color: #999; + font-size: 14px; +} +.site-footer a { + color: #3082a3; + text-decoration: underline; +} diff --git a/templates/app/app-vite/src/components/Layout/Layout.jsx b/templates/app/app-vite/src/components/Layout/Layout.jsx index 407fb917..95c1d294 100644 --- a/templates/app/app-vite/src/components/Layout/Layout.jsx +++ b/templates/app/app-vite/src/components/Layout/Layout.jsx @@ -1,58 +1,79 @@ -import { Link, Outlet } from "react-router-dom"; +import { Link, Outlet, NavLink } from "react-router-dom"; import hyfLogo from "../../assets/hyf.svg"; import { useAuth } from "../../context/AuthContext.jsx"; +import "./Layout.css"; export default function Layout() { const { user, logout } = useAuth(); return ( -
-
-