feat: scaffold Vite/React landing page and timetable UI#1
Open
Anay0305 wants to merge 33 commits into
Open
Conversation
- Vite + React 19 + react-router-dom setup - Landing page with hero (logo + brand card), batch selector, community-contributors scroller, repo link, footer, and floating navbar - /timetable/:batch page with topbar, content area, and pill-shaped navbar (batch selector, calendar/save/share, theme/profile actions) - Dev-only API plugins in vite.config.js: /api/contributors (GitHub Contributors via GITHUB_REPO/optional GITHUB_TOKEN) /api/batches (static pool/group list) - useNavbarPadding hook to offset content for fixed navbar - ESLint config (react-hooks, react-refresh) - .gitignore covers node_modules, build output, .env*, editor and OS files - .env.example documents GITHUB_REPO and optional GITHUB_TOKEN
- BatchSelector now cascades through three selects on the landing page: Year -> Stream -> Batch, populated from /api/batches - Add src/data/batches.json: alphabet -> stream-name mapping for 1st year vs years 2+ (where streams are standardized), plus 1st-year batches parsed from the official 'UG, PG TIME TABLE JAN TO MAY 2026.xlsx' (sheets '1ST YEAR A' row 5 and '1ST YEAR B' row 6) - /api/batches in vite.config.js now serves src/data/batches.json so the same payload flows from dev mock today and the real backend later - TimetablePage navbar select consumes the new shape (year+stream optgroups) - BatchSelector CSS: stack the three selects with disabled-state styling Years 2-4 are listed as 'coming soon' until the backend wires in their data.
- New src/lib/batches.js: loadBatches() tries GET ${VITE_BACKEND_URL}/batch
first; on missing var, network error, non-2xx, or non-array body it falls
back to the local src/data/batches.json
- Backend is expected to return the flat list of all batch codes across
every year/stream; loadBatches groups it client-side via groupBatches(),
which parses each "{Y}{ALPHA}{N}{N}" code, drops years/streams with no
batches, and labels streams from streamNames (1st-year-specific map for
year 1, standardized map for year 2+)
- Local fallback JSON keeps the pre-grouped 'years' shape so it can be
served as-is without re-running the grouping algorithm; flattened to
only include years that actually have data (year 1 today)
- Removed the dev-only /api/batches Vite middleware: the frontend now hits
the real backend URL directly and falls back locally on failure
- Add VITE_BACKEND_URL to .env / .env.example (VITE_ prefix exposes it to
the browser); defaults to http://localhost:8000
Backend not yet integrated — the fetch will fail silently and the local
fallback will render years/streams/batches that actually exist.
- BatchSelector + TimetablePage navbar selector now use native <input list=...> + <datalist> comboboxes, so users can type to filter options (Year, Stream, Batch on the landing page; full batch list on the timetable page with year/stream shown as the option label) - Navigation fires only when the typed value matches a valid option; changing the year/stream resets the lower fields automatically - groupBatches: stream sort is year-aware - year 1 puts Pool A then Pool B at the front and sorts the rest alphabetically; years 2+ are always alphabetical by stream code - Reorder fallback batches.json year-1 streams to A, B, G, J, R, X so the pre-grouped fallback matches the same convention without re-sorting - Favicon switched from the default Vite favicon.svg to /MLSC-logo.png and page title updated to 'MLSC Timetable'
Native <input list>+<datalist> had two limitations the user hit: - the popup is sized by the browser and was wider than the brand card on / - opening required clicking the chevron after focusing the input (two clicks) New src/components/Combobox.jsx is a lightweight searchable combobox: - single click both focuses the input and opens the list - typing filters by value / label / hint - popup is absolutely positioned within the wrapper (left:0; right:0) so it is never wider than the field itself - direction="up" flips the popup above the input (used by the bottom- anchored timetable navbar) - keyboard nav: Arrow up/down moves highlight, Enter selects, Esc closes - highlighted option auto-scrolls into view BatchSelector on / now uses three Comboboxes (Year / Stream / Batch) wired to the same cascading state as before. TimetablePage navbar uses one Combobox with direction="up" and renders each batch with a year+stream hint on the right. Dark-themed popup via .tt-batch-popup so it matches the navbar pill.
Previously navigation fired the instant the batch input matched a real code, so the user barely saw their pick before the page switched. Now we debounce with a 1s timer (cleared if the input changes again), giving visible confirmation before transitioning to /timetable/:batch.
Replaces the silent debounce with a visible loading state: once the batch input resolves to a real code, the fields lock (opacity dimmed, pointer events disabled) and a small spinner with "Loading XYZ\u2026" appears below. After ~1s navigation to /timetable/:batch fires as before. If the user changes the input mid-hold the timer is cleared and the loading state clears with it.
Wrap the top "MLSC TIMETABLE" tagline in a flex .page-header that holds an MLSC logo on the left of the text. The logo is display:none on wider screens; below 640px (where the grid was already collapsing to a single column) the logo appears in the header and the standalone .left-panel is hidden, so the brand stays visible without doubling up above the selection card.
#root sets text-align:center, so the original <p width:100%> sat centered. Moving it into a flex .page-header made it collapse to content width and snap left. Give .page-tagline flex:1 + text-align:center so it spans the available row and centers regardless of logo presence. On mobile add a :after spacer matching the logo width to keep the text optically centered.
…h grid The grid was collapsing at 640px while typical tablet portrait is 768px, so on widths in between (641-768px) the side-by-side layout was already feeling cramped while the standalone left-panel logo was still showing. Move grid collapse, header-logo reveal, and left-panel hide into the same 768px @media block so they always flip together.
Same-specificity CSS goes by source order. The base .center-grid and .left-panel declarations were defined AFTER the @media (max-width:768px) block, so they overrode the responsive overrides — the body logo and 2-column grid stayed visible on small screens. Moved the entire mobile @media block to the end of App.css so the overrides actually apply.
Was 32px which left a lot of empty padding above the brand-card. Doubling it to 56px lets the logo fill more of the top whitespace; spacer width updated to match so the tagline stays optically centered.
2.5rem/2rem padding and 2.5rem gap were generous on phones; reduce to 1rem padding and 1.25rem gap below 720px so the header, brand-card and contributors row sit closer together and waste less vertical space.
Each social link gets data-tip="..." alongside aria-label. Below 628px the inner <span> label hides and a CSS ::after tooltip (using attr(data-tip)) appears on hover/keyboard focus, matching the existing tt-tip-wrap style.
useNavbarPadding was adding 72px padding-bottom to <body> whenever the page overflowed, so the floating bottom navbar pill wouldn't cover content — but it also left a 72px empty band below the footer. Remove the hook entirely; the navbar now floats over the footer line, which is fine since the footer text sits on the far left/right and the pill is centered.
…footer Footer padding 1.25rem 2rem 0 -> 1.75rem 2rem (symmetric, ~76px tall) and navbar bottom 16px -> 13px so the 50px pill sits centered inside the footer band, lining up with the copyright/socials text on either side.
* Bottom nav defaults to a small chevron-up pill that floats over the footer without crowding it. Clicking expands the full toolbar. * While the mouse hovers the nav, it stays open. On mouseleave (or for touch, after the last tap) a 3s timer collapses it back. Hover cancels the timer; clicks/keys reset it on non-hover devices. * Top header now goes responsive at <=600px: the MLSC logo shrinks 72px -> 48px, and the absolutely-centered heading switches to flex flow with margin-left:auto so 'Timetable for XXXX' (always 4 chars) sits flush right instead of overlapping the logo.
…lose
* Track an isCompact flag via window.matchMedia('(max-width: 848px)').
At wider widths the full nav renders unconditionally and no timer
logic runs, restoring the previous desktop behaviour.
* Switched the wrapper from onMouse* to onPointerEnter/Leave with a
pointerType==='mouse' guard. Touch taps used to fire mouseenter and
permanently set the hovered flag, which kept the timer cancelled
forever on mobile. Now isMouseHoveringRef only ever flips for real
mouse pointers, so on touch the open/interact handlers always arm
the 3s close timer and tapping inside the nav resets it.
Stack the collapse toggle and the full pill in the same grid cell so opacity + scale can crossfade between them on open/close instead of an abrupt mount/unmount. The pill now stays mounted while collapsed (with inert + aria-hidden) so its enter/leave is animated, and the toggle is mounted while expanded so closing also fades smoothly. Wide viewports keep their previous behaviour via the is-wide class which never\renders the toggle.
The previous attempt left both the toggle and the full pill in normal flow, so the toggle showed off-center above the pill and the click never reached the button (also: the wrap was pill-wide even when collapsed). Now the inactive child is taken out of flow with position:absolute, opacity 0 and pointer-events:none, while the active child sizes the wrap. State is driven by is-compact + is-collapsed/is-open on the wrap, with opacity + transform transitions on both children for the crossfade.
Both the toggle and the full pill are now fixed-positioned at the same anchor (bottom 16px, centered) so swapping between them is a pure vertical slide instead of a horizontal jump caused by the wrap re-sizing. Hover/interact handlers move from the wrap onto each child, since the wrap no longer has a meaningful bounding box. collapsed -> open: pill slides up + fades in, toggle slides up + fades out open -> collapsed: pill slides down + fades out, toggle slides down + fades in
Both elements share `transform-origin: bottom center` so the pill collapses to scale(0.18) at the toggle's anchor instead of sliding off-screen. Opening grows the pill out of the chevron; closing shrinks it back into the chevron. Toggle just dissolves with a small scale-down on open.
* Outside-tap close: while the pill is expanded, listen for pointerdown on document; if the target isn't inside the pill we cancel the auto-close timer and collapse immediately. Works the same for mouse and touch. * Closing transition is now 0.45s/0.5s (vs 0.22s/0.3s opening) so the pop-back-in feels deliberate instead of snappy. The opening pop stays fast.
* Toggle is now 52x52 with border-radius:50% so it matches the pill height (~52px) — when the pill scales back into it the silhouette shrinks to the same circle. * Replaced the chevron-up SVG with a three-dot menu icon (sized 26) which reads as 'more options' rather than just 'expand'. * Folded opacity + transform transitions into the toggle's own rule so its pop-in/pop-out actually animates instead of snapping (the shared transition was being overridden by the per-element rule).
… hover * Title broken into MLSC TIMETABLE / dynamic sem subtitle / institute affiliation. Subtitle defaults to 'ODD SEM 26-27' and is replaced with whatever the backend returns from GET $VITE_BACKEND_URL/current (uses 'label' field if present, else composes from season+year). * Added 'Thapar Institute of Engineering & Tech.' as a small muted line under the sem subtitle. * Toned down .tt-icon-btn:hover: background opacity 0.20 -> 0.12, color stays close to the resting tint, plus a 1.5px lift and a soft shadow for tactile feedback (active resets both).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Initial scaffold for the MLSC timetable web app. Adds a Vite + React 19 frontend with a landing page, timetable page, and dev-only API endpoints used by the UI.
What's included
App shell
src/main.jsx— React Router setup with routes/and/timetable/:batchsrc/App.jsx— landing page (hero logo, brand card, batch selector, contributors scroller, repo link)src/pages/TimetablePage.jsx— per-batch view with topbar and pill-shaped navbar (batch select, calendar / save / share, theme / profile)src/components/Navbar.jsx+Footer.jsx— global chromesrc/components/BatchSelector.jsx— drives navigation by batchsrc/components/ContributorsScroller.jsx— auto-scrolling, hover-pause, arrow-nav contributor stripsrc/hooks/useNavbarPadding.js— keeps content clear of the fixed navbarDev-only API plugins in
vite.config.jsGET /api/contributors— proxies GitHub Contributors API for the repo named inGITHUB_REPO, optionally authenticated viaGITHUB_TOKENto avoid the 60 req/hr unauthenticated limitGET /api/batches— static pool/group list consumed by the selectorsRepo hygiene
.gitignorerewritten to covernode_modules, build output (dist,build,out), Vite cache, all.env*variants, coverage, deployment caches (.vercel,.netlify), editor and OS files.env.exampledocuments requiredGITHUB_REPOand optionalGITHUB_TOKENeslint-plugin-react-hooksandeslint-plugin-react-refreshHow to run
cp .env.example .env # then edit if you want to set GITHUB_TOKEN npm install npm run devNotes / follow-ups
vite.config.js. For production, the same endpoints will need to be served by the backend (mlsc-timetable-backend) or a serverless function.