Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions client/components/header.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,53 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';

const Header = ({ currentUser }) => {
const router = useRouter();
const [darkMode, setDarkMode] = useState(false);

// Load theme preference from localStorage
useEffect(() => {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'dark' || (!stored && prefersDark)) {
document.body.classList.add('dark');
setDarkMode(true);
}
}, []);

// Toggle dark mode
const toggleTheme = () => {
const newMode = !darkMode;
setDarkMode(newMode);
if (newMode) {
document.body.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.body.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
};

const links = [
!currentUser && { label: 'Sign Up', href: '/auth/signup' },
!currentUser && { label: 'Sign In', href: '/auth/signin' },
currentUser && { label: 'My Orders', href: '/orders' },
currentUser && { label: 'Sell Tickets', href: '/tickets/new' },
currentUser && { label: 'Sign Out', href: '/auth/signout' }
currentUser && { label: 'Sign Out', href: '/auth/signout' },
]
.filter(Boolean)
.map(({ label, href }) => {
const isActive = router.pathname === href;

return (
<li key={href} className="nav-item">
<Link
href={href}
className={`nav-link px-3 ${isActive ? 'active fw-semibold text-primary' : ''}`}
className={`nav-link px-3 fw-semibold ${
isActive
? 'text-primary border-bottom border-primary border-2'
: 'text-secondary'
}`}
>
{label}
</Link>
Expand All @@ -28,26 +56,31 @@ const Header = ({ currentUser }) => {
});

return (
<nav className="navbar navbar-expand-lg navbar-light bg-white border-bottom shadow-sm px-4">
<nav className="navbar navbar-expand-lg bg-body px-3 py-2 border-bottom shadow-sm">
<div className="container-fluid">
<Link href="/" className="navbar-brand fw-bold text-primary fs-4">
GitTix
🎫 GitTix
</Link>

<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarContent"
aria-controls="navbarContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon"></span>
<span className="navbar-toggler-icon" />
</button>

<div className="collapse navbar-collapse justify-content-end" id="navbarContent">
<ul className="navbar-nav d-flex align-items-center">{links}</ul>
<ul className="navbar-nav d-flex align-items-center gap-2">{links}</ul>

<button
className="btn btn-sm btn-outline-secondary ms-3"
onClick={toggleTheme}
title="Toggle Dark Mode"
>
{darkMode ? '🌙 Dark' : '☀️ Light'}
</button>
</div>
</div>
</nav>
Expand Down
37 changes: 37 additions & 0 deletions client/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,43 @@ body {
animation: gradientBG 15s ease infinite;
font-family: 'Segoe UI', sans-serif;
}

.nav-link:hover {
color: #0d6efd !important;
transform: translateY(-1px);
}

/* Light theme (default) */
body {
background-color: #f9fafb;
color: #212529;
}

/* Dark theme */
body.dark {
background-color: #121212;
color: #f1f1f1;
}

body.dark .navbar {
background-color: #1e1e1e !important;
}

body.dark .nav-link {
color: #ccc !important;
}

body.dark .nav-link.border-bottom {
border-color: #0d6efd !important;
color: #0d6efd !important;
}

body.dark .btn-outline-secondary {
color: #ccc;
border-color: #444;
}



@keyframes gradientBG {
0% {
Expand Down
28 changes: 20 additions & 8 deletions client/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,37 @@ import Link from "next/link";
const LandingPage = ({ currentUser, tickets }) => {
return (
<div className="container py-5">
<h2 className="mb-4 text-center">Available Tickets</h2>
<h1 className="text-center fw-bold text-primary mb-5">🎟️ Available Tickets</h1>

{tickets.length === 0 ? (
<div className="text-center text-muted">No tickets available</div>
<div className="text-center text-muted fs-5">No tickets available</div>
) : (
<div className="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
{tickets.map((ticket) => (
<div className="col" key={ticket.id}>
<div className="card shadow-sm h-100">
<div className="card border-0 shadow-sm h-100">
<div className="card-body d-flex flex-column">
<h5 className="card-title">{ticket.title}</h5>
<p className="card-text text-muted mb-4">
Price: <strong>${ticket.price}</strong>
<h5 className="text-center fw-semibold text-dark mb-3">
{ticket.title}
</h5>

<p className="text-muted mb-2">
<span className="fw-semibold">Price:</span> ${ticket.price}
</p>

<p
className="text-muted text-truncate mb-4"
style={{ maxWidth: "100%" }}
title={ticket.description}
>
<span className="fw-semibold">Description:</span> {ticket.description}
</p>

<div className="mt-auto">
<Link
href="/tickets/[ticketId]"
as={`/tickets/${ticket.id}`}
className="btn btn-outline-primary w-100"
className="btn btn-primary w-100"
>
View Ticket
</Link>
Expand All @@ -37,7 +49,7 @@ const LandingPage = ({ currentUser, tickets }) => {
};

LandingPage.getInitialProps = async (context, client, currentUser) => {
const { data } = await client.get('/api/tickets');
const { data } = await client.get("/api/tickets");
return { tickets: data };
};

Expand Down
37 changes: 25 additions & 12 deletions client/pages/tickets/[ticketId].js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,31 @@ const TicketShow = ({ ticket }) => {
});

return (
<div className="container d-flex align-items-center justify-content-center min-vh-100">
<div className="card shadow p-4" style={{ maxWidth: "500px", width: "100%" }}>
<h2 className="text-center mb-3">{ticket.title}</h2>
<hr />
<div className="mb-4">
<h5 className="text-muted">Price:</h5>
<p className="fs-4 fw-semibold">${ticket.price}</p>
</div>

{errors && <div className="alert alert-danger">{errors}</div>}

<button onClick={() => doRequest()} className="btn btn-success w-100">
<div className="container py-5" style={{ maxWidth: '800px' }}>
<div className="mb-5">
<h1 className="display-5 fw-bold text-dark mb-3">{ticket.title}</h1>
<p className="text-muted">Buy your ticket securely</p>
</div>

<div className="mb-4">
<h6 className="text-uppercase text-secondary fw-semibold mb-1">Price</h6>
<p className="fs-4 fw-semibold text-dark">${ticket.price}</p>
</div>

<div className="mb-5">
<h6 className="text-uppercase text-secondary fw-semibold mb-1">Description</h6>
<p className="fs-5 text-dark lh-lg" style={{ whiteSpace: 'pre-wrap' }}>
{ticket.description}
</p>
</div>

{errors && <div className="alert alert-danger">{errors}</div>}

<div className="d-flex justify-content-start">
<button
onClick={() => doRequest()}
className="btn btn-primary btn-lg px-4 shadow-sm"
>
Purchase Ticket
</button>
</div>
Expand Down
16 changes: 15 additions & 1 deletion client/pages/tickets/new.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import Router from "next/router";
const NewTicket = () => {
const [title, setTitle] = useState("");
const [price, setPrice] = useState("");
const [description, setDescription] = useState("");

const { doRequest, errors } = useRequest({
url: "/api/tickets",
method: "post",
body: {
title,
price
price,
description
},
onSuccess: () => {
setTitle("");
setPrice("");
setDescription("");
Router.push("/");
}
});
Expand Down Expand Up @@ -63,6 +66,17 @@ const NewTicket = () => {
/>
</div>

<div className="mb-3">
<label className="form-label">Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="form-control"
placeholder="Enter ticket description"
rows="3">
</textarea>
</div>

{errors && <div className="alert alert-danger">{errors}</div>}

<div className="d-grid">
Expand Down
2 changes: 1 addition & 1 deletion orders/src/routes/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { natsWrapper } from '../nats-wrapper';

const router = express.Router();

const EXPIRATION_WINDOW_SECONDS = 15 * 60; // 15 minutes
const EXPIRATION_WINDOW_SECONDS = 5 * 60; // 5 minutes

router.post('/api/orders', requireAuth, [
body('ticketId').not().isEmpty().custom((input: string) => mongoose.Types.ObjectId.isValid(input)).withMessage('TicketId is provided'),
Expand Down
10 changes: 8 additions & 2 deletions tickets/src/models/ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { updateIfCurrentPlugin } from 'mongoose-update-if-current';
interface TicketAttrs {
title: string;
price: number;
userId: string
userId: string;
description?: string; // description is added to the TicketAttrs interface
}

interface TicketDoc extends mongoose.Document {
Expand All @@ -13,6 +14,7 @@ interface TicketDoc extends mongoose.Document {
userId: string
version: number; // version is used by mongoose to track document versions
orderId?: string; // orderId is optional, it will be set when the ticket is reserved
description?: string; // description is added to the TicketDoc interface
}

interface TicketModel extends mongoose.Model<TicketDoc> {
Expand All @@ -36,7 +38,11 @@ const ticketSchema = new mongoose.Schema(
orderId: {
type: String,
required: false, // orderId is optional, it will be set when the ticket is reserved
}
},
description: {
type: String,
required: false, // description is optional
},
},
{
toJSON: {
Expand Down
9 changes: 5 additions & 4 deletions tickets/src/routes/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,24 @@ router.post('/api/tickets', requireAuth, [
body('price').isFloat({ gt: 0 }).withMessage('Price must be greater than 0'),

], validateRequest, async (req: Request, res: Response) => {
const { title, price } = req.body;
const { title, price, description } = req.body;

const ticket = Ticket.build({
title,
price,
userId: req.currentUser!.id
userId: req.currentUser!.id,
description, // description is optional, so it can be included or omitted
})

await ticket.save();

//Publish this event
await new TicketCreatedPublisher(natsWrapper.client).publish({
id: ticket.id,
title: ticket.title,
price: ticket.price,
userId: ticket.userId,
version: ticket.version,
version: ticket.version
});

res.status(201).send(ticket);
Expand Down
Loading