Skip to content

feat: scaffold Vite/React landing page and timetable UI#1

Open
Anay0305 wants to merge 33 commits into
MicrosoftStudentChapter:mainfrom
Anay0305:landing-page
Open

feat: scaffold Vite/React landing page and timetable UI#1
Anay0305 wants to merge 33 commits into
MicrosoftStudentChapter:mainfrom
Anay0305:landing-page

Conversation

@Anay0305

Copy link
Copy Markdown

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/:batch
  • src/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 chrome
  • src/components/BatchSelector.jsx — drives navigation by batch
  • src/components/ContributorsScroller.jsx — auto-scrolling, hover-pause, arrow-nav contributor strip
  • src/hooks/useNavbarPadding.js — keeps content clear of the fixed navbar

Dev-only API plugins in vite.config.js

  • GET /api/contributors — proxies GitHub Contributors API for the repo named in GITHUB_REPO, optionally authenticated via GITHUB_TOKEN to avoid the 60 req/hr unauthenticated limit
  • GET /api/batches — static pool/group list consumed by the selectors

Repo hygiene

  • .gitignore rewritten to cover node_modules, build output (dist, build, out), Vite cache, all .env* variants, coverage, deployment caches (.vercel, .netlify), editor and OS files
  • .env.example documents required GITHUB_REPO and optional GITHUB_TOKEN
  • ESLint config with eslint-plugin-react-hooks and eslint-plugin-react-refresh

How to run

cp .env.example .env   # then edit if you want to set GITHUB_TOKEN
npm install
npm run dev

Notes / follow-ups

  • API routes are dev-only middleware in vite.config.js. For production, the same endpoints will need to be served by the backend (mlsc-timetable-backend) or a serverless function.
  • Theme toggle and profile buttons are placeholders (no handlers yet).
  • Save / Google Calendar buttons on the timetable page are placeholders pending the timetable data model.
  • The contributors scroller falls back to a 'Github Profiles' label when the API returns nothing.

Anay0305 added 30 commits June 13, 2026 23:34
- 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant