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 (
-
- {events.map((event) => (
- -
-
{event.name}
-
- {event.date} at {event.time}
-
-
- {event.venue}, {event.city}
-
- {event.category}
- {event.price === 0 ? "Free" : `€${event.price}`}
-
- {event.ticketsAvailable === 0
- ? "Sold out"
- : `${event.ticketsAvailable} tickets left`}
-
-
- ))}
-
+
+
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 (
-
-
-