Initial Commit

This commit is contained in:
tanshu committed 2026-09-02 16:29:03 +00:00
commit 1a9c69fc9d
34 files changed
+7479

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
{
"projectName": "frontend",
"mode": "file-router",
"typescript": true,
"tailwind": true,
"packageManager": "npm",
"git": false,
"install": true,
"intent": true,
"addOnOptions": {},
"projectPreset": "default",
"includeExamples": true,
"routerOnly": false,
"version": 1,
"framework": "react",
"chosenAddOns": [
"nitro"
]
}
+13
View File
@@ -0,0 +1,13 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.env
.nitro
.tanstack
.wrangler
.output
.vinxi
__unconfig*
todos.json
+10
View File
@@ -0,0 +1,10 @@
<!-- intent-skills:start -->
## Skill Loading
Before editing files for a substantial task:
- Run `npx @tanstack/intent@latest list` from the workspace root to see available local skills.
- If a listed skill matches the task, run `npx @tanstack/intent@latest load <package>#<skill>` before changing files.
- Use the loaded `SKILL.md` guidance while making the change.
- Monorepos: when working across packages, run the skill check from the workspace root and prefer the local skill for the package being changed.
- Multiple matches: prefer the most specific local skill for the package or concern you are changing; load additional skills only when the task spans multiple packages or concerns.
<!-- intent-skills:end -->
+201
View File
@@ -0,0 +1,201 @@
Welcome to your new TanStack Start app!
# Getting Started
To run this application:
```bash
npm install
npm run dev
```
# Building For Production
To build this application for production:
```bash
npm run build
```
## Styling
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
### Removing Tailwind CSS
If you prefer not to use Tailwind CSS:
1. Remove the demo pages in `src/routes/demo/`
2. Replace the Tailwind import in `src/styles.css` with your own styles
3. Remove `tailwindcss()` from the plugins array in `vite.config.ts`
4. Remove `@tailwindcss/vite` and `tailwindcss` from `package.json`
## Deploy with Nitro
This project uses Nitro as a generic server adapter, so it can run on any Node-compatible host.
```bash
npm run build
node dist/server/index.mjs
```
The build output is a self-contained Node server. To deploy, push the `dist/` directory to your host (Render, Fly.io, your own VPS, etc.) and run the server command above.
For host-specific presets (Vercel, Netlify, Cloudflare, AWS Lambda, etc.) and tuning, see https://v3.nitro.build/deploy.
## Routing
This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`.
### Adding A Route
To add a new route to your application just add a new file in the `./src/routes` directory.
TanStack will automatically generate the content of the route file for you.
Now that you have two routes you can use a `Link` component to navigate between them.
### Adding Links
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
```tsx
import { Link } from "@tanstack/react-router";
```
Then anywhere in your JSX you can use it like so:
```tsx
<Link to="/about">About</Link>
```
This will create a link that will navigate to the `/about` route.
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
### Using A Layout
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you render `{children}` in the `shellComponent`.
Here is an example layout that includes a header:
```tsx
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'My App' },
],
}),
shellComponent: ({ children }) => (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
</header>
{children}
<Scripts />
</body>
</html>
),
})
```
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
## Server Functions
TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components.
```tsx
import { createServerFn } from '@tanstack/react-start'
const getServerTime = createServerFn({
method: 'GET',
}).handler(async () => {
return new Date().toISOString()
})
// Use in a component
function MyComponent() {
const [time, setTime] = useState('')
useEffect(() => {
getServerTime().then(setTime)
}, [])
return <div>Server time: {time}</div>
}
```
## API Routes
You can create API routes by using the `server` property in your route definitions:
```tsx
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
export const Route = createFileRoute('/api/hello')({
server: {
handlers: {
GET: () => json({ message: 'Hello, World!' }),
},
},
})
```
## Data Fetching
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
For example:
```tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/people')({
loader: async () => {
const response = await fetch('https://swapi.dev/api/people')
return response.json()
},
component: PeopleComponent,
})
function PeopleComponent() {
const data = Route.useLoaderData()
return (
<ul>
{data.results.map((person) => (
<li key={person.name}>{person.name}</li>
))}
</ul>
)
}
```
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
# Demo files
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
# Learn More
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start).
+4341
View File
File diff suppressed because it is too large. Load diff
+44
View File
@@ -0,0 +1,44 @@
{
"name": "frontend",
"private": true,
"type": "module",
"imports": {
"#/*": "./src/*"
},
"scripts": {
"dev": "vite dev --port 3000",
"generate-routes": "tsr generate",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "latest",
"@tanstack/react-router": "latest",
"@tanstack/react-router-devtools": "latest",
"@tanstack/react-start": "latest",
"lucide-react": "^1.39.0",
"nitro": "3.0.260610-beta",
"pocketbase": "^0.28.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
"@tanstack/devtools-vite": "latest",
"@tanstack/router-cli": "^1.132.0",
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^6.0.1",
"typescript": "^6.0.2",
"vite": "^8.0.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"lightningcss"
]
}
}
+90
View File
@@ -0,0 +1,90 @@
import React from 'react';
import type { Beer } from '../lib/pocketbase';
interface BeerCardProps {
beer: Beer;
onSelect?: (beer: Beer) => void;
}
export default function BeerCard({ beer, onSelect }: BeerCardProps) {
// Determine accent color style
const accentColor = beer.color_accent || '#ffb77b';
// Calculate percentage for progress bars (IBU max ~100, ABV max ~10)
const ibuPercent = Math.min(Math.max((beer.ibu / 100) * 100, 10), 100);
const abvPercent = Math.min(Math.max((beer.abv / 10) * 100, 10), 100);
return (
<div
className="bg-[#1c1b1b] border border-[#524439]/40 rounded-lg p-6 relative overflow-hidden group hover:border-[#ffb77b]/60 transition-all duration-300 flex flex-col justify-between shadow-lg hover:-translate-y-1"
onClick={() => onSelect?.(beer)}
>
{/* Top right icon badge */}
<div className="absolute top-0 right-0 w-16 h-16 bg-[#201f1f] flex items-center justify-center rounded-bl-xl border-l border-b border-[#524439]/30">
<span
className="material-symbols-outlined text-3xl"
style={{ color: accentColor }}
>
{beer.icon || 'sports_bar'}
</span>
</div>
<div>
{/* Style Badge */}
<div className="inline-block bg-[#234f3b] text-[#ffb77b] text-xs font-semibold px-3 py-1 rounded-full border border-[#ffb77b]/20 mb-4 tracking-wider uppercase">
{beer.style_tag || beer.style}
</div>
{/* Beer Name */}
<h3 className="font-['Playfair_Display'] text-2xl font-bold text-[#e5e2e1] mb-2 tracking-tight">
{beer.name}
</h3>
{/* Beer Description */}
<p className="text-[#d8c3b4] text-sm mb-6 leading-relaxed min-h-[4.5rem]">
{beer.description}
</p>
</div>
<div className="space-y-4 pt-2">
{/* IBU and ABV meters */}
<div>
<div className="flex justify-between text-xs text-[#d8c3b4] font-medium mb-1.5">
<span>IBU: <strong className="text-[#e5e2e1]">{beer.ibu}</strong></span>
<span>ABV: <strong className="text-[#e5e2e1]">{beer.abv}%</strong></span>
</div>
<div className="w-full bg-[#201f1f] h-2 rounded-full overflow-hidden border border-[#524439]/20">
<div
className="h-full rounded-full transition-all duration-500"
style={{ width: `${ibuPercent}%`, backgroundColor: accentColor }}
/>
</div>
</div>
{/* Food Pairing Footer */}
{beer.pairings && (
<div className="pt-4 border-t border-[#524439]/30 flex flex-col gap-1">
<span className="text-[11px] uppercase tracking-wider text-[#a08d80] font-semibold">
Pairs well with:
</span>
<span className="text-sm text-[#e5e2e1] font-medium">
{beer.pairings}
</span>
</div>
)}
{/* Tap Status */}
<div className="flex items-center justify-between text-xs text-[#a08d80] pt-1">
<span className="flex items-center gap-1.5">
<span className={`w-2 h-2 rounded-full ${beer.on_tap ? 'bg-emerald-400 animate-pulse' : 'bg-stone-500'}`} />
{beer.on_tap ? `Tap #${beer.tap_number || 1} Active` : 'Seasonal Special'}
</span>
{beer.keg_percentage !== undefined && (
<span>Keg: {beer.keg_percentage}%</span>
)}
</div>
</div>
</div>
);
}
+55
View File
@@ -0,0 +1,55 @@
import React from 'react';
import type { FoodItem } from '../lib/pocketbase';
interface FoodCardProps {
item: FoodItem;
}
export default function FoodCard({ item }: FoodCardProps) {
return (
<div className="bg-[#1c1b1b] border border-[#524439]/40 rounded-lg p-6 flex flex-col justify-between hover:border-[#ffb77b]/50 transition-all duration-300 shadow-md group">
<div>
<div className="flex items-center justify-between gap-2 mb-2">
<div className="flex items-center gap-2">
{/* Veg / Non-Veg Indicator */}
<span
className={`w-3.5 h-3.5 rounded-xs border flex items-center justify-center p-0.5 ${
item.is_veg
? 'border-emerald-500 text-emerald-500'
: 'border-red-500 text-red-500'
}`}
title={item.is_veg ? 'Vegetarian' : 'Non-Vegetarian'}
>
<span className={`w-1.5 h-1.5 rounded-full ${item.is_veg ? 'bg-emerald-500' : 'bg-red-500'}`} />
</span>
<span className="text-xs uppercase tracking-wider text-[#a08d80] font-semibold">
{item.category}
</span>
</div>
<span className="text-[#ffb77b] font-bold text-lg">
₹{item.price}
</span>
</div>
<h4 className="font-['Playfair_Display'] text-xl font-bold text-[#e5e2e1] mb-2 group-hover:text-[#ffb77b] transition-colors">
{item.name}
</h4>
<p className="text-[#d8c3b4] text-sm leading-relaxed mb-4">
{item.description}
</p>
</div>
{item.beer_pairing && (
<div className="pt-3 border-t border-[#524439]/20 flex items-center justify-between text-xs">
<span className="text-[#a08d80]">Recommended Brew:</span>
<span className="text-[#ffb77b] bg-[#234f3b]/80 px-2.5 py-0.5 rounded border border-[#ffb77b]/20 font-medium">
🍺 {item.beer_pairing}
</span>
</div>
)}
</div>
);
}
+128
View File
@@ -0,0 +1,128 @@
import React, { useState } from 'react';
import { Link } from '@tanstack/react-router';
import { subscribeNewsletter } from '../lib/pocketbase';
interface FooterProps {
onOpenReserve?: () => void;
}
export default function Footer({ onOpenReserve }: FooterProps) {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const handleSubscribe = async (e: React.FormEvent) => {
e.preventDefault();
if (!email) return;
setStatus('loading');
const res = await subscribeNewsletter(email);
if (res.success) {
setStatus('success');
setEmail('');
} else {
setStatus('error');
}
};
return (
<footer className="bg-[#0e0e0e] w-full py-16 sm:py-20 border-t border-[#524439]/30 text-[#e5e2e1]">
<div className="grid grid-cols-1 md:grid-cols-3 gap-10 px-4 sm:px-8 md:px-16 max-w-[1200px] mx-auto">
{/* Brand & Mission */}
<div className="space-y-4">
<Link to="/" className="font-['Playfair_Display'] text-2xl font-bold text-[#ffb77b] block">
HOPS N GRAINS
</Link>
<p className="text-sm text-[#d8c3b4] max-w-xs leading-relaxed">
Handcrafted in Chandigarh & Panchkula. A unique blend of industrial heritage and artisanal microbrewing science.
</p>
<div className="pt-2 text-xs text-[#a08d80] space-y-1">
<p>📍 Sector 9-D, Madhya Marg, Chandigarh</p>
<p>📍 SCO 358, Sector 9, Panchkula, Haryana</p>
</div>
</div>
{/* Links */}
<div>
<h5 className="text-sm font-semibold tracking-wider uppercase text-[#ffb77b] mb-4">
Navigation
</h5>
<ul className="space-y-2.5 text-sm text-[#d8c3b4]">
<li>
<Link to="/brews" className="hover:text-[#ffb77b] transition-colors hover:translate-x-1 inline-block duration-200">
Our Brews & Taps
</Link>
</li>
<li>
<Link to="/food" className="hover:text-[#ffb77b] transition-colors hover:translate-x-1 inline-block duration-200">
Global Comfort Food
</Link>
</li>
<li>
<Link to="/brewery" className="hover:text-[#ffb77b] transition-colors hover:translate-x-1 inline-block duration-200">
The Brewery & Craft
</Link>
</li>
<li>
<Link to="/live" className="hover:text-[#ffb77b] transition-colors hover:translate-x-1 inline-block duration-200">
Live Tap Board
</Link>
</li>
</ul>
</div>
{/* Newsletter & Reserve CTA */}
<div className="flex flex-col justify-between space-y-6">
<div>
<h5 className="text-sm font-semibold tracking-wider uppercase text-[#ffb77b] mb-2">
Join The Craft Club
</h5>
<p className="text-xs text-[#d8c3b4] mb-3">
Receive notifications on seasonal brew releases and tap room events.
</p>
{status === 'success' ? (
<div className="bg-[#234f3b] text-[#ffb77b] text-xs p-3 rounded border border-[#ffb77b]/30">
✓ You're on the list! Welcome to the craft club.
</div>
) : (
<form onSubmit={handleSubscribe} className="flex gap-2">
<input
type="email"
required
placeholder="Your email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="bg-[#1c1b1b] border border-[#524439]/60 rounded px-3 py-2 text-xs text-[#e5e2e1] flex-1 focus:outline-none focus:border-[#ffb77b]"
/>
<button
type="submit"
disabled={status === 'loading'}
className="border border-[#ffb77b]/40 text-[#ffb77b] text-xs font-semibold px-4 py-2 rounded hover:bg-[#ffb77b]/10 transition-colors disabled:opacity-50 cursor-pointer"
>
{status === 'loading' ? '...' : 'Join'}
</button>
</form>
)}
{status === 'error' && (
<p className="text-xs text-red-400 mt-1">Something went wrong. Please try again.</p>
)}
</div>
{onOpenReserve && (
<div>
<button
onClick={onOpenReserve}
className="bg-[#201f1f] border border-[#ffb77b]/40 text-[#ffb77b] px-6 py-2.5 rounded hover:bg-[#ffb77b]/10 transition-colors text-sm font-semibold cursor-pointer"
>
Reserve a Table
</button>
</div>
)}
<p className="text-xs text-[#a08d80]">
© {new Date().getFullYear()} Hops n Grains Microbrewery. All rights reserved.
</p>
</div>
</div>
</footer>
);
}
+116
View File
@@ -0,0 +1,116 @@
import React, { useState } from 'react';
import { Link, useRouterState } from '@tanstack/react-router';
interface NavbarProps {
onOpenReserve: () => void;
}
export default function Navbar({ onOpenReserve }: NavbarProps) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const router = useRouterState();
const currentPath = router.location.pathname;
const navLinks = [
{ label: 'Our Brews', path: '/brews' },
{ label: 'Global Comfort Food', path: '/food' },
{ label: 'The Brewery', path: '/brewery' },
{ label: 'Live On Tap', path: '/live' },
];
return (
<>
<header className="fixed top-0 w-full z-40 bg-[#131313]/85 backdrop-blur-xl border-b border-[#524439]/20 shadow-sm transition-all duration-300 ease-in-out">
<div className="flex justify-between items-center px-4 sm:px-8 md:px-16 max-w-[1200px] mx-auto h-[72px]">
{/* Brand Logo */}
<Link
to="/"
className="font-['Playfair_Display'] text-2xl font-bold tracking-tight text-[#ffb77b] hover:opacity-90 transition-opacity"
>
HOPS N GRAINS
</Link>
{/* Desktop Nav Links */}
<nav className="hidden md:flex items-center space-x-6">
{navLinks.map((link) => {
const isActive = currentPath === link.path;
return (
<Link
key={link.path}
to={link.path}
className={`text-sm font-semibold tracking-wider transition-colors px-3 py-1.5 rounded ${
isActive
? 'text-[#ffb77b] border-b-2 border-[#ffb77b]'
: 'text-[#d8c3b4] hover:text-[#ffb77b] hover:bg-[#353534]/20'
}`}
>
{link.label}
</Link>
);
})}
</nav>
{/* Reserve Now Button & Mobile Hamburger */}
<div className="flex items-center gap-3">
<button
onClick={onOpenReserve}
className="bg-[#ffb77b] text-[#4d2700] text-sm font-semibold px-5 py-2.5 rounded hover:bg-[#ffdcc2] transition-all duration-200 inner-shadow-copper shadow-sm cursor-pointer"
>
Reserve Now
</button>
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden text-[#ffb77b] p-1.5 rounded hover:bg-[#201f1f] transition-colors"
aria-label="Toggle navigation menu"
>
<span className="material-symbols-outlined text-2xl">
{mobileMenuOpen ? 'close' : 'menu'}
</span>
</button>
</div>
</div>
</header>
{/* Mobile Drawer */}
{mobileMenuOpen && (
<div className="fixed inset-0 z-30 pt-[72px] bg-[#131313]/95 backdrop-blur-2xl md:hidden animate-fade-in flex flex-col justify-between p-6">
<div className="space-y-4">
<Link
to="/"
onClick={() => setMobileMenuOpen(false)}
className={`block py-3 px-4 rounded-lg text-lg font-semibold ${
currentPath === '/' ? 'bg-[#234f3b] text-[#ffb77b]' : 'text-[#e5e2e1]'
}`}
>
Home
</Link>
{navLinks.map((link) => (
<Link
key={link.path}
to={link.path}
onClick={() => setMobileMenuOpen(false)}
className={`block py-3 px-4 rounded-lg text-lg font-semibold ${
currentPath === link.path ? 'bg-[#234f3b] text-[#ffb77b]' : 'text-[#e5e2e1]'
}`}
>
{link.label}
</Link>
))}
</div>
<div className="pt-6 border-t border-[#524439]/30">
<button
onClick={() => {
setMobileMenuOpen(false);
onOpenReserve();
}}
className="w-full bg-[#ffb77b] text-[#4d2700] font-semibold py-3.5 rounded-md inner-shadow-copper text-center cursor-pointer"
>
Reserve a Table
</button>
</div>
</div>
)}
</>
);
}
@@ -0,0 +1,247 @@
import React, { useState } from 'react';
import { createReservation, type ReservationData } from '../lib/pocketbase';
interface ReservationModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function ReservationModal({ isOpen, onClose }: ReservationModalProps) {
const [formData, setFormData] = useState<ReservationData>({
name: '',
email: '',
phone: '',
date: new Date().toISOString().split('T')[0],
time: '19:30',
guests: 2,
location: 'Chandigarh (Sector 9)',
special_requests: '',
});
const [loading, setLoading] = useState(false);
const [confirmedId, setConfirmedId] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
if (!isOpen) return null;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setErrorMessage(null);
const res = await createReservation(formData);
setLoading(false);
if (res.success && res.id) {
setConfirmedId(res.id);
} else {
setErrorMessage(res.error || 'Failed to submit reservation. Please try again.');
}
};
const handleReset = () => {
setConfirmedId(null);
setErrorMessage(null);
onClose();
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md animate-fade-in">
<div
className="bg-[#1c1b1b] border border-[#ffb77b]/40 rounded-xl max-w-lg w-full p-6 sm:p-8 relative shadow-2xl overflow-y-auto max-h-[90vh]"
onClick={(e) => e.stopPropagation()}
>
{/* Close button */}
<button
onClick={handleReset}
className="absolute top-4 right-4 text-[#a08d80] hover:text-[#ffb77b] transition-colors p-2 rounded-full hover:bg-[#201f1f]"
>
<span className="material-symbols-outlined text-2xl">close</span>
</button>
{confirmedId ? (
<div className="text-center py-8 space-y-4">
<div className="w-16 h-16 rounded-full bg-[#234f3b] text-[#ffb77b] flex items-center justify-center mx-auto border border-[#ffb77b]/30">
<span className="material-symbols-outlined text-3xl">check_circle</span>
</div>
<h3 className="font-['Playfair_Display'] text-2xl font-bold text-[#e5e2e1]">
Table Reserved!
</h3>
<p className="text-[#d8c3b4] text-sm max-w-sm mx-auto">
We look forward to welcoming you to Hops n Grains ({formData.location}). A confirmation message will be sent to <strong>{formData.phone}</strong>.
</p>
<div className="bg-[#201f1f] p-3 rounded-lg border border-[#524439]/40 inline-block text-xs font-mono text-[#ffb77b]">
Booking ID: #{confirmedId}
</div>
<div className="pt-4">
<button
onClick={handleReset}
className="bg-[#ffb77b] text-[#4d2700] font-semibold px-6 py-2.5 rounded hover:bg-[#ffdcc2] transition-colors inner-shadow-copper"
>
Done
</button>
</div>
</div>
) : (
<div>
<div className="mb-6">
<span className="text-xs uppercase tracking-widest text-[#ffb77b] font-semibold">
Table Booking
</span>
<h2 className="font-['Playfair_Display'] text-2xl sm:text-3xl font-bold text-[#e5e2e1] mt-1">
Reserve Your Experience
</h2>
<p className="text-[#d8c3b4] text-sm mt-1">
Handcrafted beer & artisanal dining at Chandigarh & Panchkula.
</p>
</div>
{errorMessage && (
<div className="mb-4 p-3 rounded bg-red-950/50 border border-red-800 text-red-300 text-xs">
{errorMessage}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{/* Location selector */}
<div>
<label className="block text-xs uppercase tracking-wider text-[#a08d80] font-semibold mb-1">
Brewery Branch *
</label>
<select
value={formData.location}
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
className="w-full bg-[#201f1f] border border-[#524439]/60 rounded-md px-3.5 py-2.5 text-sm text-[#e5e2e1] focus:outline-none focus:border-[#ffb77b]"
required
>
<option value="Chandigarh (Sector 9)">Hops n Grains — Sector 9, Chandigarh</option>
<option value="Panchkula (Sector 5)">Hops n Grains — Sector 5, Panchkula</option>
</select>
</div>
{/* Name & Phone */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs uppercase tracking-wider text-[#a08d80] font-semibold mb-1">
Your Name *
</label>
<input
type="text"
required
placeholder="e.g. Tanshu"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full bg-[#201f1f] border border-[#524439]/60 rounded-md px-3.5 py-2.5 text-sm text-[#e5e2e1] focus:outline-none focus:border-[#ffb77b]"
/>
</div>
<div>
<label className="block text-xs uppercase tracking-wider text-[#a08d80] font-semibold mb-1">
Phone Number *
</label>
<input
type="tel"
required
placeholder="+91 98765 43210"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="w-full bg-[#201f1f] border border-[#524439]/60 rounded-md px-3.5 py-2.5 text-sm text-[#e5e2e1] focus:outline-none focus:border-[#ffb77b]"
/>
</div>
</div>
{/* Email */}
<div>
<label className="block text-xs uppercase tracking-wider text-[#a08d80] font-semibold mb-1">
Email Address *
</label>
<input
type="email"
required
placeholder="name@example.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full bg-[#201f1f] border border-[#524439]/60 rounded-md px-3.5 py-2.5 text-sm text-[#e5e2e1] focus:outline-none focus:border-[#ffb77b]"
/>
</div>
{/* Date, Time, Guests */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label className="block text-xs uppercase tracking-wider text-[#a08d80] font-semibold mb-1">
Date *
</label>
<input
type="date"
required
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
className="w-full bg-[#201f1f] border border-[#524439]/60 rounded-md px-3 py-2 text-sm text-[#e5e2e1] focus:outline-none focus:border-[#ffb77b]"
/>
</div>
<div>
<label className="block text-xs uppercase tracking-wider text-[#a08d80] font-semibold mb-1">
Time *
</label>
<input
type="time"
required
value={formData.time}
onChange={(e) => setFormData({ ...formData, time: e.target.value })}
className="w-full bg-[#201f1f] border border-[#524439]/60 rounded-md px-3 py-2 text-sm text-[#e5e2e1] focus:outline-none focus:border-[#ffb77b]"
/>
</div>
<div>
<label className="block text-xs uppercase tracking-wider text-[#a08d80] font-semibold mb-1">
Guests *
</label>
<select
value={formData.guests}
onChange={(e) => setFormData({ ...formData, guests: Number(e.target.value) })}
className="w-full bg-[#201f1f] border border-[#524439]/60 rounded-md px-3 py-2 text-sm text-[#e5e2e1] focus:outline-none focus:border-[#ffb77b]"
>
{[1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 16, 20].map((n) => (
<option key={n} value={n}>
{n} {n === 1 ? 'Guest' : 'Guests'}
</option>
))}
</select>
</div>
</div>
{/* Special Requests */}
<div>
<label className="block text-xs uppercase tracking-wider text-[#a08d80] font-semibold mb-1">
Special Requests (Optional)
</label>
<textarea
rows={2}
placeholder="e.g., Table near copper brewing tanks, birthday anniversary celebration"
value={formData.special_requests}
onChange={(e) => setFormData({ ...formData, special_requests: e.target.value })}
className="w-full bg-[#201f1f] border border-[#524439]/60 rounded-md px-3.5 py-2 text-sm text-[#e5e2e1] focus:outline-none focus:border-[#ffb77b]"
/>
</div>
<div className="pt-3">
<button
type="submit"
disabled={loading}
className="w-full bg-[#ffb77b] text-[#4d2700] font-semibold px-6 py-3.5 rounded-md hover:bg-[#ffdcc2] transition-colors inner-shadow-copper flex items-center justify-center gap-2 shadow-lg disabled:opacity-50 cursor-pointer"
>
{loading ? (
<>
<span className="w-4 h-4 border-2 border-[#4d2700] border-t-transparent rounded-full animate-spin" />
Securing Table...
</>
) : (
'Confirm Table Reservation'
)}
</button>
</div>
</form>
</div>
)}
</div>
</div>
);
}
+73
View File
@@ -0,0 +1,73 @@
import React from 'react';
interface TapHandleProps {
label: string;
value: number;
min: number;
max: number;
step?: number;
onChange: (val: number) => void;
unit?: string;
}
export default function TapHandle({
label,
value,
min,
max,
step = 1,
onChange,
unit = '',
}: TapHandleProps) {
const percentage = Math.round(((value - min) / (max - min)) * 100);
return (
<div className="bg-[#1c1b1b] border border-[#524439]/40 rounded-lg p-5 flex flex-col items-center select-none shadow-md">
<span className="text-xs uppercase tracking-wider text-[#a08d80] font-semibold mb-2">
{label}
</span>
{/* Display Value */}
<div className="text-2xl font-bold font-['Playfair_Display'] text-[#ffb77b] mb-4">
{value} {unit}
</div>
{/* Industrial Tap Handle Slider Graphic */}
<div className="relative w-12 h-44 bg-[#0e0e0e] rounded-full p-1.5 border border-[#524439]/60 flex flex-col justify-end items-center overflow-hidden">
{/* Amber / Beer Liquid Fill */}
<div
className="w-full rounded-full transition-all duration-300 relative bg-gradient-to-t from-[#c8803f] via-[#FFBF00] to-[#ffb77b]"
style={{ height: `${percentage}%` }}
>
{/* Froth line */}
<div className="w-full h-2 bg-amber-100/90 rounded-full blur-[0.5px]" />
</div>
{/* Copper Ferrule / Handle Grip */}
<div
className="absolute inset-x-1.5 h-7 rounded-sm bg-gradient-to-r from-[#8c4f10] via-[#ffb77b] to-[#8c4f10] border border-[#ffb77b]/50 shadow-md cursor-grab active:cursor-grabbing flex items-center justify-center transition-all duration-150"
style={{ bottom: `calc(${percentage}% - 14px)` }}
>
<div className="w-6 h-1 bg-[#4d2700]/60 rounded-full" />
</div>
{/* Hidden Range Input for full accessibility and drag */}
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="absolute inset-0 opacity-0 cursor-pointer h-full w-full"
aria-label={label}
/>
</div>
<div className="flex justify-between w-full text-[10px] text-[#a08d80] mt-3 font-semibold">
<span>Min: {min}</span>
<span>Max: {max}</span>
</div>
</div>
);
}
+273
View File
@@ -0,0 +1,273 @@
import PocketBase from 'pocketbase';
export interface Beer {
id: string;
name: string;
slug: string;
style: string;
style_tag: string;
description: string;
ibu: number;
abv: number;
pairings: string;
icon: string;
color_accent: string;
on_tap: boolean;
tap_number: number;
keg_percentage: number;
}
export interface FoodItem {
id: string;
name: string;
category: string;
description: string;
price: number;
is_veg: boolean;
beer_pairing?: string;
}
export interface ReservationData {
name: string;
email: string;
phone: string;
date: string;
time: string;
guests: number;
location: string;
special_requests?: string;
}
export const FALLBACK_BEERS: Beer[] = [
{
id: 'b1',
name: 'Weissbier',
slug: 'weissbier',
style: 'German Wheat Beer',
style_tag: 'Hefeweizen',
description: 'A classic German wheat beer with distinct notes of clove and banana. Unfiltered, hazy, and incredibly refreshing.',
ibu: 15,
abv: 5.2,
pairings: 'Citrus Salads, Mild Cheeses',
icon: 'local_drink',
color_accent: '#ffb77b',
on_tap: true,
tap_number: 1,
keg_percentage: 82,
},
{
id: 'b2',
name: 'Premium Pilsner',
slug: 'premium-pilsner',
style: 'Bohemian Lager',
style_tag: 'Lager',
description: 'Crisp, clean, and brilliantly clear. Brewed with noble hops for a floral aroma and a snappy, dry finish.',
ibu: 35,
abv: 4.8,
pairings: 'Spicy Appetizers, Seafood',
icon: 'sports_bar',
color_accent: '#FFBF00',
on_tap: true,
tap_number: 2,
keg_percentage: 64,
},
{
id: 'b3',
name: 'Industrial IPA',
slug: 'industrial-ipa',
style: 'India Pale Ale',
style_tag: 'IPA',
description: 'A hop-forward ale bursting with tropical fruit and pine aromas. Balanced by a sturdy malt backbone.',
ibu: 65,
abv: 6.5,
pairings: 'Rich Curries, Grilled Meats',
icon: 'liquor',
color_accent: '#efbf78',
on_tap: true,
tap_number: 3,
keg_percentage: 45,
},
{
id: 'b4',
name: 'Oatmeal Stout',
slug: 'oatmeal-stout',
style: 'Robust Dark Ale',
style_tag: 'Dark Ale',
description: 'Velvety smooth with rich flavors of espresso and dark chocolate. A comforting, robust brew.',
ibu: 25,
abv: 5.5,
pairings: 'Chocolate Desserts, BBQ',
icon: 'local_cafe',
color_accent: '#4B3621',
on_tap: true,
tap_number: 4,
keg_percentage: 30,
},
{
id: 'b5',
name: 'Belgian Dubbel',
slug: 'belgian-dubbel',
style: 'Abbey Ale',
style_tag: 'Belgian Ale',
description: 'Rich and complex with candied fruit, raisin, and caramel sweetness with a warm malty finish.',
ibu: 22,
abv: 7.0,
pairings: 'Roasted Duck, Aged Gouda',
icon: 'wine_bar',
color_accent: '#b48948',
on_tap: true,
tap_number: 5,
keg_percentage: 90,
},
{
id: 'b6',
name: 'Apple Cider',
slug: 'apple-cider',
style: 'Handcrafted Fruit Cider',
style_tag: 'Cider',
description: 'Pressed Himalayan apples fermented to a sparkling, crisp balance of tartness and crisp apple blossom.',
ibu: 8,
abv: 4.5,
pairings: 'Pork Ribs, Camembert, Salads',
icon: 'eco',
color_accent: '#a2d1b7',
on_tap: true,
tap_number: 6,
keg_percentage: 75,
},
];
export const FALLBACK_FOOD: FoodItem[] = [
{
id: 'f1',
name: 'Truffle Parmesan Fries',
category: 'Appetizers',
description: 'Hand-cut rustic fries tossed with white truffle oil, shaved Grana Padano, and fresh rosemary aioli.',
price: 345,
is_veg: true,
beer_pairing: 'Weissbier',
},
{
id: 'f2',
name: 'Smoked Chicken Wings',
category: 'Appetizers',
description: 'Hickory wood-smoked wings glazed with house-made IPA barbecue sauce, served with blue cheese dip.',
price: 495,
is_veg: false,
beer_pairing: 'Industrial IPA',
},
{
id: 'f3',
name: 'Wood-Fired Margherita Pizza',
category: 'Pizzas',
description: 'San Marzano tomatoes, fresh Fior di Latte mozzarella, fragrant sweet basil, and extra virgin olive oil.',
price: 545,
is_veg: true,
beer_pairing: 'Premium Pilsner',
},
{
id: 'f4',
name: 'Pepperoni & Hot Honey Pizza',
category: 'Pizzas',
description: 'Spicy artisanal pepperoni, fresh mozzarella, and a drizzle of chilli-infused organic honey.',
price: 645,
is_veg: false,
beer_pairing: 'Industrial IPA',
},
{
id: 'f5',
name: "The Brewer's Smash Burger",
category: 'Mains',
description: 'Double tenderloin smashed patties, melted aged cheddar, caramelized beer onions, dill pickles on brioche.',
price: 595,
is_veg: false,
beer_pairing: 'Industrial IPA',
},
{
id: 'f6',
name: 'Grilled Cottage Cheese Steak',
category: 'Mains',
description: 'Herb-marinated paneer steak grilled over charcoal, roasted root vegetables, and a creamy pepper sauce.',
price: 525,
is_veg: true,
beer_pairing: 'Weissbier',
},
{
id: 'f7',
name: 'Dark Chocolate Stout Brownie',
category: 'Desserts',
description: 'Warm Belgian chocolate brownie infused with Oatmeal Stout, topped with Madagascan vanilla bean gelato.',
price: 375,
is_veg: true,
beer_pairing: 'Oatmeal Stout',
},
];
const getPbUrl = () => {
if (typeof window !== 'undefined') {
return `${window.location.protocol}//${window.location.hostname}:8090`;
}
return 'http://127.0.0.1:8090';
};
export const pb = new PocketBase(getPbUrl());
pb.autoCancellation(false);
export async function fetchBeers(): Promise<Beer[]> {
try {
const res = await pb.collection('beers').getFullList<Beer>({
sort: 'tap_number',
requestKey: null,
});
if (res && res.length > 0) {
return res;
}
} catch (err) {
console.warn('Could not fetch beers from PocketBase, using default craft list:', err);
}
return FALLBACK_BEERS;
}
export async function fetchFoodItems(): Promise<FoodItem[]> {
try {
const res = await pb.collection('food_items').getFullList<FoodItem>({
sort: 'category',
requestKey: null,
});
if (res && res.length > 0) {
return res;
}
} catch (err) {
console.warn('Could not fetch food items from PocketBase, using default list:', err);
}
return FALLBACK_FOOD;
}
export async function createReservation(data: ReservationData): Promise<{ success: boolean; id?: string; error?: string }> {
try {
const record = await pb.collection('reservations').create({
...data,
status: 'pending',
}, { requestKey: null });
return { success: true, id: record.id };
} catch (err: any) {
console.error('Error submitting reservation:', err);
return { success: false, error: err?.message || 'Failed to submit reservation' };
}
}
export async function subscribeNewsletter(email: string): Promise<{ success: boolean; error?: string }> {
try {
await pb.collection('subscribers').create({
email,
active: true,
}, { requestKey: null });
return { success: true };
} catch (err: any) {
if (err?.status === 400 && err?.response?.data?.email?.code === 'validation_not_unique') {
return { success: true };
}
console.error('Error subscribing to newsletter:', err);
return { success: false, error: err?.message || 'Subscription failed' };
}
}
+140
View File
@@ -0,0 +1,140 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as BreweryRouteImport } from './routes/brewery'
import { Route as BrewsRouteImport } from './routes/brews'
import { Route as FoodRouteImport } from './routes/food'
import { Route as LiveRouteImport } from './routes/live'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const BreweryRoute = BreweryRouteImport.update({
id: '/brewery',
path: '/brewery',
getParentRoute: () => rootRouteImport,
} as any)
const BrewsRoute = BrewsRouteImport.update({
id: '/brews',
path: '/brews',
getParentRoute: () => rootRouteImport,
} as any)
const FoodRoute = FoodRouteImport.update({
id: '/food',
path: '/food',
getParentRoute: () => rootRouteImport,
} as any)
const LiveRoute = LiveRouteImport.update({
id: '/live',
path: '/live',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/brewery': typeof BreweryRoute
'/brews': typeof BrewsRoute
'/food': typeof FoodRoute
'/live': typeof LiveRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/brewery': typeof BreweryRoute
'/brews': typeof BrewsRoute
'/food': typeof FoodRoute
'/live': typeof LiveRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/brewery': typeof BreweryRoute
'/brews': typeof BrewsRoute
'/food': typeof FoodRoute
'/live': typeof LiveRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/brewery' | '/brews' | '/food' | '/live'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/brewery' | '/brews' | '/food' | '/live'
id: '__root__' | '/' | '/brewery' | '/brews' | '/food' | '/live'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
BreweryRoute: typeof BreweryRoute
BrewsRoute: typeof BrewsRoute
FoodRoute: typeof FoodRoute
LiveRoute: typeof LiveRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/brewery': {
id: '/brewery'
path: '/brewery'
fullPath: '/brewery'
preLoaderRoute: typeof BreweryRouteImport
parentRoute: typeof rootRouteImport
}
'/brews': {
id: '/brews'
path: '/brews'
fullPath: '/brews'
preLoaderRoute: typeof BrewsRouteImport
parentRoute: typeof rootRouteImport
}
'/food': {
id: '/food'
path: '/food'
fullPath: '/food'
preLoaderRoute: typeof FoodRouteImport
parentRoute: typeof rootRouteImport
}
'/live': {
id: '/live'
path: '/live'
fullPath: '/live'
preLoaderRoute: typeof LiveRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
BreweryRoute: BreweryRoute,
BrewsRoute: BrewsRoute,
FoodRoute: FoodRoute,
LiveRoute: LiveRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
+19
View File
@@ -0,0 +1,19 @@
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function getRouter() {
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
})
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}
+84
View File
@@ -0,0 +1,84 @@
import React, { useState, createContext, useContext } from 'react';
import { HeadContent, Outlet, Scripts, createRootRoute } from '@tanstack/react-router';
import Navbar from '../components/Navbar';
import Footer from '../components/Footer';
import ReservationModal from '../components/ReservationModal';
import appCss from '../styles.css?url';
interface ReservationContextType {
openReservation: () => void;
closeReservation: () => void;
}
const ReservationContext = createContext<ReservationContextType>({
openReservation: () => {},
closeReservation: () => {},
});
export const useReservation = () => useContext(ReservationContext);
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'Hops n Grains | Handcrafted Microbrewery' },
{
name: 'description',
content: 'Experience artisanal craft brewing and global comfort dining in Chandigarh & Panchkula. Handcrafted heritage, poured daily.',
},
],
links: [
{ rel: 'stylesheet', href: appCss },
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: 'anonymous' },
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,600;0,700;1,600;1,700&display=swap',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap',
},
],
}),
shellComponent: RootDocument,
component: RootLayout,
});
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark scroll-smooth">
<head>
<HeadContent />
</head>
<body className="bg-[#131313] text-[#e5e2e1] min-h-screen texture-wood antialiased selection:bg-[#ffb77b]/30 selection:text-[#ffb77b]">
{children}
<Scripts />
</body>
</html>
);
}
function RootLayout() {
const [isReserveOpen, setIsReserveOpen] = useState(false);
return (
<ReservationContext.Provider
value={{
openReservation: () => setIsReserveOpen(true),
closeReservation: () => setIsReserveOpen(false),
}}
>
<div className="flex flex-col min-h-screen">
<Navbar onOpenReserve={() => setIsReserveOpen(true)} />
<main className="flex-1">
<Outlet />
</main>
<Footer onOpenReserve={() => setIsReserveOpen(true)} />
<ReservationModal isOpen={isReserveOpen} onClose={() => setIsReserveOpen(false)} />
</div>
</ReservationContext.Provider>
);
}
+169
View File
@@ -0,0 +1,169 @@
import React from 'react';
import { createFileRoute } from '@tanstack/react-router';
import { useReservation } from './__root';
export const Route = createFileRoute('/brewery')({
component: BreweryPage,
});
function BreweryPage() {
const { openReservation } = useReservation();
const steps = [
{
num: '01',
title: 'Mashing & Wort Extraction',
desc: 'Imported Bavarian two-row barley and wheat malts are gently milled and steeped in temperature-controlled mash tuns to convert starches into fermentable sugars.',
icon: 'grain',
},
{
num: '02',
title: 'Kettle Boiling & Noble Hops',
desc: 'The sweet wort is brought to a vigorous rolling boil in our gleaming copper kettles, adding floral Hallertau and bold Pacific Northwest hops for aroma and bitterness.',
icon: 'local_fire_department',
},
{
num: '03',
title: 'Fermentation & Yeast Maturation',
desc: 'Wort is cooled and pitched with pure liquid yeast strains in cylindrical-conical fermentation vessels for 14 to 28 days of slow, cold conditioning.',
icon: 'science',
},
{
num: '04',
title: 'Fresh Taproom Cellar Pouring',
desc: 'Unpasteurized and unfiltered, the beer travels directly through chilled copper piping straight into your glass at optimum carbonation and temperature.',
icon: 'sports_bar',
},
];
return (
<div className="pt-28 pb-24 max-w-[1200px] mx-auto px-4 sm:px-8 md:px-16">
{/* Header */}
<section className="text-center mb-16 space-y-4">
<div className="inline-block bg-[#234f3b]/70 border border-[#ffb77b]/30 px-3.5 py-1 rounded-full text-xs font-semibold text-[#ffb77b] uppercase tracking-widest mb-2">
Industrial Craft
</div>
<h1 className="font-['Playfair_Display'] text-4xl sm:text-5xl md:text-6xl font-bold text-[#ffb77b] tracking-tight">
The Brewery & Heritage
</h1>
<p className="font-['Montserrat'] text-base sm:text-lg text-[#d8c3b4] max-w-2xl mx-auto leading-relaxed">
Pioneering the craft brewing revolution in northern India with authentic German brewing science, unpasteurized natural beers, and time-honored techniques.
</p>
</section>
{/* Hero Image / Video Banner */}
<div className="rounded-2xl overflow-hidden border border-[#524439]/50 shadow-2xl relative mb-20">
<img
src="https://lh3.googleusercontent.com/aida-public/AB6AXuDUSzRNpNL9yRO68697o8lFyYvHKeNEN81LzmidWX_Bx-oi5gCLBug3Zd8gtovPWOisZaW85dQfY_6h02_h0ISWDR-kTszZF4v_qshihZzDbD7mfJFvjNbykV9I8n29vzdDGsl6JjK-9-ksacMATK00NCHU6uYvvcUDuttvcuJTlVyjz8A-9Ph04AUrWKIOz708W-MzHDpFWuqWns_JQPabpyL3u6RzH0py-MCuTvveng7W3r5s4c1x"
alt="Hops n Grains brewing vessels"
className="w-full h-80 sm:h-[420px] object-cover filter brightness-85"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#131313] via-[#131313]/30 to-transparent" />
<div className="absolute bottom-8 left-8 right-8 max-w-xl">
<span className="text-xs uppercase tracking-widest text-[#ffb77b] font-semibold">
Copper & Wood
</span>
<h3 className="font-['Playfair_Display'] text-2xl sm:text-3xl font-bold text-[#e5e2e1] mt-1">
Built from scratch for uncompromised beer purity.
</h3>
</div>
</div>
{/* 4-Step Brewing Process */}
<section className="mb-24">
<div className="text-center max-w-xl mx-auto mb-12">
<span className="text-xs uppercase tracking-widest text-[#ffb77b] font-semibold block mb-1">
The Science
</span>
<h2 className="font-['Playfair_Display'] text-3xl font-bold text-[#e5e2e1]">
From Grain to Glass
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{steps.map((s) => (
<div
key={s.num}
className="bg-[#1c1b1b] border border-[#524439]/40 rounded-xl p-6 relative flex flex-col justify-between hover:border-[#ffb77b]/50 transition-colors shadow-lg"
>
<div>
<div className="flex justify-between items-center mb-4">
<span className="font-['Playfair_Display'] text-2xl font-bold text-[#ffb77b]">
{s.num}
</span>
<span className="material-symbols-outlined text-[#ffb77b] text-2xl">
{s.icon}
</span>
</div>
<h4 className="font-['Playfair_Display'] text-lg font-bold text-[#e5e2e1] mb-2">
{s.title}
</h4>
<p className="text-xs text-[#d8c3b4] leading-relaxed">
{s.desc}
</p>
</div>
</div>
))}
</div>
</section>
{/* Taproom Locations */}
<section className="bg-[#1c1b1b] border border-[#524439]/40 rounded-2xl p-8 sm:p-12 mb-16 shadow-xl">
<h2 className="font-['Playfair_Display'] text-3xl font-bold text-[#ffb77b] text-center mb-10">
Our Taproom Locations
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Chandigarh */}
<div className="bg-[#201f1f] border border-[#524439]/30 rounded-xl p-6 space-y-4">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-[#ffb77b] text-3xl">location_on</span>
<div>
<h3 className="font-['Playfair_Display'] text-xl font-bold text-[#e5e2e1]">
Chandigarh Flagship
</h3>
<span className="text-xs text-[#a08d80]">Sector 9-D, Madhya Marg</span>
</div>
</div>
<p className="text-sm text-[#d8c3b4] leading-relaxed">
Our premier taproom features an expansive two-story layout showcasing our active copper brewhouse vats right behind the bar, paired with warm brick masonry and rustic wooden booths.
</p>
<div className="text-xs text-[#a08d80] pt-2 border-t border-[#524439]/30 space-y-1">
<p>🕒 Open Daily: 12:00 PM – 12:00 Midnight</p>
<p>📞 Phone: +91 172 400 3999</p>
</div>
</div>
{/* Panchkula */}
<div className="bg-[#201f1f] border border-[#524439]/30 rounded-xl p-6 space-y-4">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-[#ffb77b] text-3xl">location_on</span>
<div>
<h3 className="font-['Playfair_Display'] text-xl font-bold text-[#e5e2e1]">
Panchkula Taproom
</h3>
<span className="text-xs text-[#a08d80]">SCO 358, Sector 9</span>
</div>
</div>
<p className="text-sm text-[#d8c3b4] leading-relaxed">
The original Hops n Grains home that started the craft beer journey in Haryana. Intimate ambient lighting, live acoustic sessions, and our signature cold pours.
</p>
<div className="text-xs text-[#a08d80] pt-2 border-t border-[#524439]/30 space-y-1">
<p>🕒 Open Daily: 12:00 PM – 12:00 Midnight</p>
<p>📞 Phone: +91 172 400 2999</p>
</div>
</div>
</div>
<div className="text-center pt-8">
<button
onClick={openReservation}
className="bg-[#ffb77b] text-[#4d2700] font-semibold px-8 py-3.5 rounded hover:bg-[#ffdcc2] transition-colors inner-shadow-copper shadow-md cursor-pointer"
>
Reserve at Either Location
</button>
</div>
</section>
</div>
);
}
+154
View File
@@ -0,0 +1,154 @@
import React, { useEffect, useState, useMemo } from 'react';
import { createFileRoute } from '@tanstack/react-router';
import BeerCard from '../components/BeerCard';
import TapHandle from '../components/TapHandle';
import { fetchBeers, type Beer } from '../lib/pocketbase';
import { useReservation } from './__root';
export const Route = createFileRoute('/brews')({
component: BrewsPage,
});
function BrewsPage() {
const [beers, setBeers] = useState<Beer[]>([]);
const [loading, setLoading] = useState(true);
const [selectedTag, setSelectedTag] = useState<string>('all');
const [maxIbuFilter, setMaxIbuFilter] = useState<number>(70);
const { openReservation } = useReservation();
useEffect(() => {
fetchBeers().then((data) => {
setBeers(data);
setLoading(false);
});
}, []);
const tags = useMemo(() => {
const set = new Set<string>();
beers.forEach((b) => {
if (b.style_tag) set.add(b.style_tag);
});
return ['all', ...Array.from(set)];
}, [beers]);
const filteredBeers = useMemo(() => {
return beers.filter((b) => {
const matchTag = selectedTag === 'all' || b.style_tag === selectedTag;
const matchIbu = b.ibu <= maxIbuFilter;
return matchTag && matchIbu;
});
}, [beers, selectedTag, maxIbuFilter]);
return (
<div className="pt-28 pb-24 max-w-[1200px] mx-auto px-4 sm:px-8 md:px-16">
{/* Hero Section matching Google Stitch Mockup */}
<section className="text-center mb-16 space-y-4">
<div className="inline-block bg-[#234f3b]/70 border border-[#ffb77b]/30 px-3.5 py-1 rounded-full text-xs font-semibold text-[#ffb77b] uppercase tracking-widest mb-2">
Microbrewery Taproom
</div>
<h1 className="font-['Playfair_Display'] text-4xl sm:text-5xl md:text-6xl font-bold text-[#ffb77b] tracking-tight">
Masterfully Brewed. Carefully Crafted.
</h1>
<p className="font-['Montserrat'] text-base sm:text-lg text-[#d8c3b4] max-w-2xl mx-auto leading-relaxed">
Discover the perfect harmony of artisanal brewing and global culinary traditions. Every pint tells a story, every plate is an exploration.
</p>
</section>
{/* Specialty Component & Interactive Filter Bar */}
<div className="bg-[#1c1b1b] border border-[#524439]/40 rounded-xl p-6 mb-16 shadow-xl">
<div className="flex flex-col lg:flex-row items-center justify-between gap-8">
{/* Style Chips */}
<div className="space-y-3 w-full lg:w-auto">
<span className="text-xs uppercase tracking-wider text-[#a08d80] font-semibold block">
Filter by Style
</span>
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<button
key={tag}
onClick={() => setSelectedTag(tag)}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-200 cursor-pointer ${
selectedTag === tag
? 'bg-[#ffb77b] text-[#4d2700] inner-shadow-copper shadow-md'
: 'bg-[#201f1f] text-[#d8c3b4] border border-[#524439]/40 hover:border-[#ffb77b]/50'
}`}
>
{tag === 'all' ? 'All Brews' : tag}
</button>
))}
</div>
</div>
{/* Interactive Tap Handle Slider: Bitterness (IBU) */}
<div className="flex items-center gap-6 w-full lg:w-auto justify-center">
<TapHandle
label="Bitterness Filter (IBU)"
value={maxIbuFilter}
min={10}
max={70}
step={5}
onChange={setMaxIbuFilter}
unit="IBU"
/>
</div>
</div>
</div>
{/* The Brews Section Divider matching Google Stitch */}
<section className="mb-20">
<div className="flex items-center gap-4 mb-12">
<div className="h-[1px] bg-[#ffb77b]/30 flex-1" />
<h2 className="font-['Playfair_Display'] text-2xl sm:text-3xl md:text-4xl text-[#ffb77b] font-bold">
Our Craft
</h2>
<div className="h-[1px] bg-[#ffb77b]/30 flex-1" />
</div>
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-96 bg-[#1c1b1b] border border-[#524439]/30 rounded-lg animate-pulse" />
))}
</div>
) : filteredBeers.length === 0 ? (
<div className="text-center py-16 bg-[#1c1b1b] border border-[#524439]/30 rounded-lg">
<p className="text-[#d8c3b4] text-base mb-4">No brews found matching the selected IBU or style criteria.</p>
<button
onClick={() => {
setSelectedTag('all');
setMaxIbuFilter(70);
}}
className="text-xs uppercase tracking-wider font-semibold text-[#ffb77b] underline cursor-pointer"
>
Reset Filters
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{filteredBeers.map((beer) => (
<BeerCard key={beer.id} beer={beer} />
))}
</div>
)}
</section>
{/* Tasting Flight Callout */}
<div className="bg-[#201f1f] border border-[#524439]/40 rounded-xl p-8 text-center sm:text-left sm:flex items-center justify-between gap-6 shadow-xl">
<div className="space-y-2 mb-4 sm:mb-0">
<h3 className="font-['Playfair_Display'] text-2xl font-bold text-[#ffb77b]">
Can't Decide? Try the Brewer's Sampler Flight
</h3>
<p className="text-sm text-[#d8c3b4] max-w-xl leading-relaxed">
Experience 4 distinct 150ml pours of your choice, complete with pairing notes curated by our Head Brewmaster.
</p>
</div>
<button
onClick={openReservation}
className="bg-[#ffb77b] text-[#4d2700] text-sm font-semibold px-6 py-3 rounded hover:bg-[#ffdcc2] transition-colors inner-shadow-copper shadow whitespace-nowrap cursor-pointer"
>
Reserve Tasting Table
</button>
</div>
</div>
);
}
+132
View File
@@ -0,0 +1,132 @@
import React, { useEffect, useState, useMemo } from 'react';
import { createFileRoute } from '@tanstack/react-router';
import FoodCard from '../components/FoodCard';
import { fetchFoodItems, type FoodItem } from '../lib/pocketbase';
import { useReservation } from './__root';
export const Route = createFileRoute('/food')({
component: FoodPage,
});
function FoodPage() {
const [foodItems, setFoodItems] = useState<FoodItem[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [vegOnly, setVegOnly] = useState<boolean>(false);
const { openReservation } = useReservation();
useEffect(() => {
fetchFoodItems().then((data) => {
setFoodItems(data);
setLoading(false);
});
}, []);
const categories = useMemo(() => {
const set = new Set<string>();
foodItems.forEach((f) => set.add(f.category));
return ['all', ...Array.from(set)];
}, [foodItems]);
const filteredItems = useMemo(() => {
return foodItems.filter((item) => {
const matchCategory = selectedCategory === 'all' || item.category === selectedCategory;
const matchVeg = !vegOnly || item.is_veg;
return matchCategory && matchVeg;
});
}, [foodItems, selectedCategory, vegOnly]);
return (
<div className="pt-28 pb-24 max-w-[1200px] mx-auto px-4 sm:px-8 md:px-16">
{/* Header */}
<section className="text-center mb-16 space-y-4">
<div className="inline-block bg-[#234f3b]/70 border border-[#ffb77b]/30 px-3.5 py-1 rounded-full text-xs font-semibold text-[#ffb77b] uppercase tracking-widest mb-2">
Global Comfort Kitchen
</div>
<h1 className="font-['Playfair_Display'] text-4xl sm:text-5xl md:text-6xl font-bold text-[#ffb77b] tracking-tight">
Global Comfort Food
</h1>
<p className="font-['Montserrat'] text-base sm:text-lg text-[#d8c3b4] max-w-2xl mx-auto leading-relaxed">
Culinary creations crafted specifically to complement the aromatic hop notes, malt body, and crisp carbonation of our house-brewed beers.
</p>
</section>
{/* Filter Bar */}
<div className="bg-[#1c1b1b] border border-[#524439]/40 rounded-xl p-6 mb-12 flex flex-col sm:flex-row items-center justify-between gap-4 shadow-lg">
{/* Category Pills */}
<div className="flex flex-wrap gap-2">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-200 cursor-pointer ${
selectedCategory === cat
? 'bg-[#ffb77b] text-[#4d2700] inner-shadow-copper shadow-md'
: 'bg-[#201f1f] text-[#d8c3b4] border border-[#524439]/40 hover:border-[#ffb77b]/50'
}`}
>
{cat === 'all' ? 'All Dishes' : cat}
</button>
))}
</div>
{/* Veg Only Toggle */}
<label className="flex items-center gap-2.5 cursor-pointer text-xs font-semibold uppercase tracking-wider text-[#e5e2e1] select-none bg-[#201f1f] px-3.5 py-2 rounded-lg border border-[#524439]/40">
<input
type="checkbox"
checked={vegOnly}
onChange={(e) => setVegOnly(e.target.checked)}
className="rounded border-[#524439] text-[#234f3b] focus:ring-[#ffb77b]"
/>
<span>🌱 Vegetarian Only</span>
</label>
</div>
{/* Food Grid */}
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="h-64 bg-[#1c1b1b] border border-[#524439]/30 rounded-lg animate-pulse" />
))}
</div>
) : filteredItems.length === 0 ? (
<div className="text-center py-16 bg-[#1c1b1b] border border-[#524439]/30 rounded-lg">
<p className="text-[#d8c3b4] text-base mb-4">No dishes found in this category.</p>
<button
onClick={() => {
setSelectedCategory('all');
setVegOnly(false);
}}
className="text-xs uppercase tracking-wider font-semibold text-[#ffb77b] underline cursor-pointer"
>
Reset Filters
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16">
{filteredItems.map((item) => (
<FoodCard key={item.id} item={item} />
))}
</div>
)}
{/* Reserve Callout */}
<div className="bg-[#1c1b1b] border border-[#ffb77b]/30 rounded-xl p-8 text-center space-y-4 shadow-xl">
<h3 className="font-['Playfair_Display'] text-2xl sm:text-3xl font-bold text-[#e5e2e1]">
Join Us for Lunch or Dinner
</h3>
<p className="text-[#d8c3b4] text-sm max-w-xl mx-auto">
Reserve your table in Chandigarh or Panchkula to guarantee seating for your party during busy evening taproom hours.
</p>
<div className="pt-2">
<button
onClick={openReservation}
className="bg-[#ffb77b] text-[#4d2700] font-semibold px-8 py-3.5 rounded hover:bg-[#ffdcc2] transition-colors inner-shadow-copper shadow-md cursor-pointer"
>
Reserve a Table
</button>
</div>
</div>
</div>
);
}
+216
View File
@@ -0,0 +1,216 @@
import React, { useEffect, useState } from 'react';
import { createFileRoute, Link } from '@tanstack/react-router';
import BeerCard from '../components/BeerCard';
import { fetchBeers, type Beer } from '../lib/pocketbase';
import { useReservation } from './__root';
export const Route = createFileRoute('/')({
component: HomePage,
});
function HomePage() {
const [beers, setBeers] = useState<Beer[]>([]);
const [loading, setLoading] = useState(true);
const { openReservation } = useReservation();
useEffect(() => {
fetchBeers().then((data) => {
setBeers(data);
setLoading(false);
});
}, []);
return (
<div>
{/* Hero Section matching Google Stitch Home Design */}
<section className="relative min-h-[92vh] flex items-center justify-center overflow-hidden pt-16">
{/* Background image: Authentic dark brewery with copper vats */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-45 transform scale-105 transition-transform duration-10000"
style={{
backgroundImage: `url('https://lh3.googleusercontent.com/aida-public/AB6AXuDUSzRNpNL9yRO68697o8lFyYvHKeNEN81LzmidWX_Bx-oi5gCLBug3Zd8gtovPWOisZaW85dQfY_6h02_h0ISWDR-kTszZF4v_qshihZzDbD7mfJFvjNbykV9I8n29vzdDGsl6JjK-9-ksacMATK00NCHU6uYvvcUDuttvcuJTlVyjz8A-9Ph04AUrWKIOz708W-MzHDpFWuqWns_JQPabpyL3u6RzH0py-MCuTvveng7W3r5s4c1x')`,
}}
/>
{/* Ambient Gradient Overlays */}
<div className="absolute inset-0 bg-gradient-to-t from-[#131313] via-[#131313]/60 to-[#131313]/40" />
<div className="absolute inset-0 bg-radial from-transparent via-[#131313]/40 to-[#131313]/90" />
<div className="relative z-10 max-w-[1200px] mx-auto px-4 sm:px-8 md:px-16 text-center space-y-8 py-20">
<div className="inline-flex items-center gap-2 bg-[#234f3b]/70 border border-[#ffb77b]/30 px-4 py-1.5 rounded-full text-xs font-semibold text-[#ffb77b] uppercase tracking-widest backdrop-blur-md">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
Fresh Batches Flowing Daily
</div>
<h1 className="font-['Playfair_Display'] text-4xl sm:text-6xl md:text-[76px] leading-[1.1] text-[#e5e2e1] max-w-4xl mx-auto drop-shadow-2xl">
Handcrafted Heritage,<br />
<span className="text-[#ffb77b] italic font-normal">Poured Daily.</span>
</h1>
<p className="font-['Montserrat'] text-base sm:text-lg md:text-xl text-[#d8c3b4] max-w-2xl mx-auto leading-relaxed">
Experience the precise intersection of industrial brewing science and rustic hospitality in Chandigarh & Panchkula.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4">
<Link
to="/brews"
className="w-full sm:w-auto bg-[#ffb77b] text-[#4d2700] font-semibold text-sm sm:text-base px-8 py-4 rounded-md hover:bg-[#ffdcc2] transition-all duration-300 inner-shadow-copper transform hover:-translate-y-0.5 shadow-xl text-center cursor-pointer"
>
Explore Our Brews
</Link>
<button
onClick={openReservation}
className="w-full sm:w-auto bg-[#201f1f]/80 backdrop-blur-md border border-[#ffb77b]/40 text-[#ffb77b] hover:bg-[#ffb77b]/10 font-semibold text-sm sm:text-base px-8 py-4 rounded-md transition-all duration-300 transform hover:-translate-y-0.5 shadow-xl cursor-pointer"
>
Reserve a Table
</button>
</div>
</div>
</section>
{/* Featured Craft Brews Preview */}
<section className="max-w-[1200px] mx-auto px-4 sm:px-8 md:px-16 py-24">
<div className="text-center max-w-2xl mx-auto mb-16">
<span className="text-xs uppercase tracking-widest text-[#ffb77b] font-semibold block mb-2">
The Microbrewery Collection
</span>
<h2 className="font-['Playfair_Display'] text-3xl sm:text-4xl md:text-5xl font-bold text-[#e5e2e1] mb-4">
Masterfully Brewed. Carefully Crafted.
</h2>
<p className="text-[#d8c3b4] text-sm sm:text-base leading-relaxed">
From cloudy Bavarian Weissbiers to boldly dry-hopped IPAs, our beers are brewed on-site in gleaming copper tanks following authentic European purity standards.
</p>
</div>
{/* Copper Section Line */}
<div className="flex items-center gap-4 mb-12">
<div className="h-[1px] bg-[#ffb77b]/30 flex-1" />
<span className="font-['Playfair_Display'] text-xl text-[#ffb77b] italic">Signature Pours</span>
<div className="h-[1px] bg-[#ffb77b]/30 flex-1" />
</div>
{loading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-80 bg-[#1c1b1b] border border-[#524439]/30 rounded-lg animate-pulse" />
))}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{beers.slice(0, 4).map((beer) => (
<BeerCard key={beer.id} beer={beer} />
))}
</div>
)}
<div className="text-center mt-12">
<Link
to="/brews"
className="inline-flex items-center gap-2 text-sm uppercase tracking-wider font-semibold text-[#ffb77b] hover:text-[#ffdcc2] transition-colors border-b border-[#ffb77b]/40 pb-1"
>
View All 6 Seasonal Brews on Tap
<span className="material-symbols-outlined text-base">arrow_forward</span>
</Link>
</div>
</section>
{/* Industrial Heritage & Science Section */}
<section className="bg-[#1c1b1b]/60 border-y border-[#524439]/30 py-24 relative overflow-hidden">
<div className="max-w-[1200px] mx-auto px-4 sm:px-8 md:px-16">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<span className="text-xs uppercase tracking-widest text-[#ffb77b] font-semibold">
Brewing Science
</span>
<h2 className="font-['Playfair_Display'] text-3xl sm:text-4xl md:text-5xl font-bold text-[#e5e2e1] leading-tight">
Authentic Craftsmanship Meets Modern Utility.
</h2>
<p className="text-[#d8c3b4] text-base leading-relaxed">
At Hops n Grains, beer isn’t an afterthought—it’s the cornerstone of our identity. Established in Chandigarh and Panchkula as the region's premier microbrewery, our brewing equipment is handcrafted in Germany, utilizing unpasteurized methods that preserve essential flavors and delicate aromatics.
</p>
<div className="grid grid-cols-2 gap-6 pt-4">
<div className="border-l-2 border-[#ffb77b] pl-4">
<div className="font-['Playfair_Display'] text-2xl sm:text-3xl font-bold text-[#ffb77b]">
100%
</div>
<div className="text-xs text-[#d8c3b4] uppercase tracking-wider font-semibold mt-1">
Imported Bavarian Malts
</div>
</div>
<div className="border-l-2 border-[#234f3b] pl-4">
<div className="font-['Playfair_Display'] text-2xl sm:text-3xl font-bold text-[#a2d1b7]">
0%
</div>
<div className="text-xs text-[#d8c3b4] uppercase tracking-wider font-semibold mt-1">
Artificial Additives
</div>
</div>
</div>
<div className="pt-4">
<Link
to="/brewery"
className="bg-[#201f1f] text-[#ffb77b] border border-[#ffb77b]/30 text-sm font-semibold px-6 py-3 rounded hover:bg-[#ffb77b]/10 transition-colors inline-block"
>
Discover The Brewery Process
</Link>
</div>
</div>
<div className="relative">
<div className="rounded-xl overflow-hidden border border-[#524439]/50 shadow-2xl relative">
<img
src="https://lh3.googleusercontent.com/aida-public/AB6AXuDUSzRNpNL9yRO68697o8lFyYvHKeNEN81LzmidWX_Bx-oi5gCLBug3Zd8gtovPWOisZaW85dQfY_6h02_h0ISWDR-kTszZF4v_qshihZzDbD7mfJFvjNbykV9I8n29vzdDGsl6JjK-9-ksacMATK00NCHU6uYvvcUDuttvcuJTlVyjz8A-9Ph04AUrWKIOz708W-MzHDpFWuqWns_JQPabpyL3u6RzH0py-MCuTvveng7W3r5s4c1x"
alt="Microbrewery copper tanks"
className="w-full h-96 object-cover object-center filter brightness-90 hover:scale-105 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#131313] via-transparent to-transparent" />
</div>
{/* Badge overlay */}
<div className="absolute -bottom-6 -left-6 bg-[#201f1f] border border-[#ffb77b]/40 rounded-lg p-4 shadow-xl hidden sm:flex items-center gap-3">
<span className="material-symbols-outlined text-[#ffb77b] text-3xl">verified</span>
<div>
<div className="text-xs font-bold text-[#e5e2e1]">Certified Master Brewer</div>
<div className="text-[11px] text-[#a08d80]">Reinheitsgebot Purity Standard</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Global Comfort Food Teaser */}
<section className="max-w-[1200px] mx-auto px-4 sm:px-8 md:px-16 py-24">
<div className="bg-gradient-to-r from-[#1c1b1b] via-[#201f1f] to-[#1c1b1b] border border-[#524439]/40 rounded-2xl p-8 sm:p-12 relative overflow-hidden flex flex-col md:flex-row items-center justify-between gap-8 shadow-xl">
<div className="space-y-4 max-w-xl">
<span className="text-xs uppercase tracking-widest text-[#ffb77b] font-semibold">
Culinary Pairings
</span>
<h3 className="font-['Playfair_Display'] text-3xl sm:text-4xl font-bold text-[#e5e2e1]">
Global Comfort Food, Crafted For Craft Beer.
</h3>
<p className="text-[#d8c3b4] text-sm sm:text-base leading-relaxed">
From thin-crust wood-fired pizzas to juicy tenderloin smash burgers and artisanal platters, every recipe is dialed in to elevate the nuanced hop and malt profiles of our pours.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 w-full md:w-auto">
<Link
to="/food"
className="bg-[#ffb77b] text-[#4d2700] text-center font-semibold px-6 py-3.5 rounded hover:bg-[#ffdcc2] transition-colors inner-shadow-copper shadow-md text-sm whitespace-nowrap"
>
Browse Food Menu
</Link>
<button
onClick={openReservation}
className="bg-[#131313] border border-[#ffb77b]/40 text-[#ffb77b] text-center font-semibold px-6 py-3.5 rounded hover:bg-[#ffb77b]/10 transition-colors text-sm whitespace-nowrap cursor-pointer"
>
Book Table for Dinner
</button>
</div>
</div>
</section>
</div>
);
}
+190
View File
@@ -0,0 +1,190 @@
import React, { useEffect, useState } from 'react';
import { createFileRoute } from '@tanstack/react-router';
import { fetchBeers, type Beer } from '../lib/pocketbase';
import { useReservation } from './__root';
export const Route = createFileRoute('/live')({
component: LiveTapPage,
});
function LiveTapPage() {
const [beers, setBeers] = useState<Beer[]>([]);
const [loading, setLoading] = useState(true);
const [lastRefreshed, setLastRefreshed] = useState<Date>(new Date());
const { openReservation } = useReservation();
const loadData = () => {
setLoading(true);
fetchBeers().then((data) => {
setBeers(data);
setLoading(false);
setLastRefreshed(new Date());
});
};
useEffect(() => {
loadData();
}, []);
return (
<div className="pt-28 pb-24 max-w-[1200px] mx-auto px-4 sm:px-8 md:px-16">
{/* Header */}
<section className="text-center mb-12 space-y-4">
<div className="inline-flex items-center gap-2 bg-[#234f3b] border border-[#ffb77b]/40 px-4 py-1.5 rounded-full text-xs font-semibold text-[#ffb77b] uppercase tracking-widest mb-2">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400 animate-ping" />
Live Cellar Telemetry
</div>
<h1 className="font-['Playfair_Display'] text-4xl sm:text-5xl md:text-6xl font-bold text-[#ffb77b] tracking-tight">
Live On Tap Board
</h1>
<p className="font-['Montserrat'] text-base sm:text-lg text-[#d8c3b4] max-w-2xl mx-auto leading-relaxed">
Real-time draft line status directly from our cellar tanks. Check current keg volume and tap availability before you arrive.
</p>
<div className="flex items-center justify-center gap-3 text-xs text-[#a08d80] pt-2">
<span>Updated: {lastRefreshed.toLocaleTimeString()}</span>
<span>•</span>
<button
onClick={loadData}
className="text-[#ffb77b] hover:underline flex items-center gap-1 cursor-pointer"
>
<span className="material-symbols-outlined text-sm">refresh</span>
Refresh Status
</button>
</div>
</section>
{/* Industrial Taproom Board */}
<div className="bg-[#1c1b1b] border-2 border-[#524439]/60 rounded-2xl p-6 sm:p-10 shadow-2xl relative overflow-hidden mb-16">
{/* Brass Header Badge */}
<div className="flex flex-col sm:flex-row items-center justify-between pb-6 mb-8 border-b border-[#524439]/40 gap-4">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-[#ffb77b] text-3xl">sports_bar</span>
<div>
<h2 className="font-['Playfair_Display'] text-2xl font-bold text-[#e5e2e1]">
Active Taps & Keg Volume
</h2>
<span className="text-xs text-[#a08d80]">Sector 9 Chandigarh & Panchkula Lines</span>
</div>
</div>
<div className="flex items-center gap-4 text-xs font-semibold">
<span className="flex items-center gap-1.5 text-emerald-400">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400" />
Tapping Fresh
</span>
<span className="flex items-center gap-1.5 text-[#ffb77b]">
<span className="w-2.5 h-2.5 rounded-full bg-[#ffb77b]" />
Limited Reserve
</span>
</div>
</div>
{/* Tap rows */}
{loading ? (
<div className="space-y-4">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="h-24 bg-[#201f1f] rounded-lg animate-pulse" />
))}
</div>
) : (
<div className="space-y-4">
{beers.map((beer) => {
const keg = beer.keg_percentage ?? 70;
const isLow = keg < 35;
const accent = beer.color_accent || '#ffb77b';
return (
<div
key={beer.id}
className="bg-[#201f1f]/90 border border-[#524439]/40 hover:border-[#ffb77b]/50 rounded-xl p-4 sm:p-6 transition-all duration-300 flex flex-col md:flex-row items-start md:items-center justify-between gap-6 group"
>
{/* Tap Number & Beer Info */}
<div className="flex items-center gap-4 sm:gap-6 flex-1">
{/* Big Tap Number Pill */}
<div className="w-12 h-12 rounded-lg bg-[#131313] border border-[#ffb77b]/30 flex flex-col items-center justify-center shrink-0">
<span className="text-[10px] uppercase font-bold text-[#a08d80]">Tap</span>
<span className="font-['Playfair_Display'] text-xl font-bold text-[#ffb77b] leading-none">
{beer.tap_number || '•'}
</span>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="font-['Playfair_Display'] text-xl font-bold text-[#e5e2e1] group-hover:text-[#ffb77b] transition-colors">
{beer.name}
</h3>
<span className="bg-[#234f3b] text-[#ffb77b] text-[11px] font-semibold px-2.5 py-0.5 rounded-full border border-[#ffb77b]/20 uppercase">
{beer.style_tag || beer.style}
</span>
</div>
<p className="text-xs text-[#d8c3b4] max-w-lg line-clamp-1">
{beer.description}
</p>
</div>
</div>
{/* Specs & Keg Level Meter */}
<div className="flex items-center gap-6 w-full md:w-auto justify-between md:justify-end border-t md:border-t-0 border-[#524439]/30 pt-3 md:pt-0">
{/* ABV & IBU */}
<div className="text-right text-xs space-y-0.5">
<div className="text-[#e5e2e1] font-bold">{beer.abv}% ABV</div>
<div className="text-[#a08d80]">{beer.ibu} IBU</div>
</div>
{/* Keg Progress Visual */}
<div className="w-36 sm:w-44 space-y-1.5">
<div className="flex justify-between text-[11px] font-semibold">
<span className="text-[#a08d80]">Keg Remaining</span>
<span className={isLow ? 'text-amber-400 font-bold' : 'text-[#ffb77b]'}>
{keg}% {isLow ? '(Low)' : ''}
</span>
</div>
<div className="w-full bg-[#131313] h-2.5 rounded-full overflow-hidden border border-[#524439]/30">
<div
className="h-full rounded-full transition-all duration-700"
style={{
width: `${keg}%`,
backgroundColor: isLow ? '#FFBF00' : accent,
}}
/>
</div>
</div>
{/* Status Dot */}
<div className="hidden sm:block">
<span
className={`inline-block w-3 h-3 rounded-full ${
beer.on_tap ? 'bg-emerald-400 animate-pulse' : 'bg-stone-500'
}`}
title={beer.on_tap ? 'Tap Line Active' : 'Off Tap'}
/>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
{/* Call to action */}
<div className="bg-[#201f1f] border border-[#ffb77b]/30 rounded-xl p-8 text-center space-y-4 shadow-xl">
<h3 className="font-['Playfair_Display'] text-2xl sm:text-3xl font-bold text-[#e5e2e1]">
Fancy a Cold Pint?
</h3>
<p className="text-[#d8c3b4] text-sm max-w-lg mx-auto">
Walk-ins are always welcomed, but table reservations are recommended during Friday and weekend peak hours.
</p>
<div>
<button
onClick={openReservation}
className="bg-[#ffb77b] text-[#4d2700] font-semibold px-8 py-3 rounded hover:bg-[#ffdcc2] transition-colors inner-shadow-copper shadow-md cursor-pointer"
>
Reserve Your Table Now
</button>
</div>
</div>
</div>
);
}
+89
View File
@@ -0,0 +1,89 @@
@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,600;0,700;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap");
@import "tailwindcss";
@theme {
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--color-background: #131313;
--color-surface: #131313;
--color-surface-dim: #131313;
--color-surface-bright: #393939;
--color-surface-container-lowest: #0e0e0e;
--color-surface-container-low: #1c1b1b;
--color-surface-container: #201f1f;
--color-surface-container-high: #2a2a2a;
--color-surface-container-highest: #353534;
--color-surface-variant: #353534;
--color-on-surface: #e5e2e1;
--color-on-surface-variant: #d8c3b4;
--color-outline: #a08d80;
--color-outline-variant: #524439;
--color-primary: #ffb77b;
--color-on-primary: #4d2700;
--color-primary-container: #c8803f;
--color-on-primary-container: #432100;
--color-primary-fixed: #ffdcc2;
--color-primary-fixed-dim: #ffb77b;
--color-secondary: #a2d1b7;
--color-on-secondary: #083825;
--color-secondary-container: #234f3b;
--color-on-secondary-container: #91bfa6;
--color-tertiary: #efbf78;
--color-on-tertiary: #442c00;
--color-tertiary-container: #b48948;
--color-amber-highlight: #FFBF00;
--color-warm-earth: #4B3621;
--color-aged-charcoal: #1A1A1A;
}
:root {
color-scheme: dark;
}
body {
background-color: #131313;
color: #e5e2e1;
font-family: var(--font-body);
margin: 0;
padding: 0;
}
.texture-bg {
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100' height='100' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E");
}
.texture-wood {
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.05' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100' height='100' filter='url(%23noise)' opacity='0.05'/%3E%3C/svg%3E");
background-color: #131313;
}
.inner-shadow-copper {
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.5), inset 0 1px 0 0 rgba(184, 115, 51, 0.3);
}
.inset-shadow-copper {
box-shadow: inset 0 2px 4px rgba(184, 115, 51, 0.2), inset 0 -2px 4px rgba(0, 0, 0, 0.5);
}
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
font-feature-settings: 'liga';
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
+28
View File
@@ -0,0 +1,28 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"paths": {
"#/*": ["./src/*"],
"@/*": ["./src/*"]
},
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"target": "react"
}
+21
View File
@@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import { devtools } from '@tanstack/devtools-vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { nitro } from 'nitro/vite'
const config = defineConfig({
resolve: { tsconfigPaths: true },
plugins: [
devtools(),
nitro({ rollupConfig: { external: [/^@sentry\//] } }),
tailwindcss(),
tanstackStart(),
viteReact(),
],
})
export default config