A complete beginner's guide to understanding every line of code in the Austin Karaoke Directory
The Austin Karaoke Directory is what developers call a "Single Page Application" or SPA. This means that instead of loading a new page every time you click something, JavaScript updates the content on the same page. This makes the app feel faster and more responsive, similar to how apps on your phone work.
This application is built with three core web technologies that every website uses:
Many modern websites use frameworks like React, Vue, or Angular. These are pre-built libraries that make development faster but add complexity. This project uses "vanilla JavaScript" - meaning plain JavaScript without any framework. This approach has several benefits:
Understanding how files are organized is crucial for navigating any codebase. Here's how this project is structured:
Key Concept: Separation of Concerns
Notice how different types of code are in different folders. This is a fundamental principle called "separation of concerns" - keeping code organized by what it does. This makes it easier to find things and prevents the codebase from becoming a tangled mess.
HTML is written using "tags" - special keywords surrounded by angle brackets. Tags usually come in pairs: an opening tag and a closing tag. Everything between them is the content. For example, <p>Hello</p> creates a paragraph containing the word "Hello".
Every HTML file starts with <!DOCTYPE html>. This isn't actually an HTML tag - it's an instruction to the web browser saying "this is an HTML5 document." HTML5 is the current version of HTML. Without this line, browsers might render your page in an older compatibility mode, which can cause layout problems.
After the DOCTYPE, we have <html lang="en">. This is the root element - everything else goes inside it. The lang="en" attribute tells browsers and assistive technologies (like screen readers for visually impaired users) that the page content is in English. This helps with pronunciation in text-to-speech and affects things like spell-checking.
The <head> section contains metadata - information ABOUT the page that doesn't appear visually on screen. This includes:
The line <meta charset="UTF-8"> tells the browser how to interpret the text characters in your file. UTF-8 is a universal encoding that supports virtually every language and symbol, including emojis. Without this, special characters might display as garbage (like "é" instead of "é").
The viewport meta tag is critical for mobile devices: <meta name="viewport" content="width=device-width, initial-scale=1.0">. Before smartphones, websites were designed for desktop monitors. When mobile browsers first loaded these sites, they would show a tiny zoomed-out version. The viewport tag tells the browser "make the page width match the device width, and don't zoom in or out initially." Without this, your responsive designs won't work on mobile.
The <title> tag sets the text that appears in the browser tab. It's also what shows up in search engine results and when someone bookmarks your page. It should be descriptive but concise - search engines typically show about 50-60 characters.
We use <link rel="stylesheet" href="..."> to connect CSS files to our HTML. The browser loads these in order, which matters because CSS rules can override each other. Our app loads Font Awesome (for icons) first, then our custom stylesheets in a specific order: base (variables and resets), layout (structure), components (UI pieces), and views (page-specific styles).
The <body> section contains everything users actually see on the page. In our app, this is organized into several main areas:
Instead of using generic <div> tags for everything, we use "semantic" elements that describe their purpose:
Using semantic elements helps screen readers understand your page structure, improves SEO (search engine optimization), and makes your code more readable to other developers.
You'll see attributes like data-song="0" or data-venue-id="..." throughout the HTML. These "data attributes" let us store custom information on HTML elements that JavaScript can read later. Any attribute starting with data- is accessible in JavaScript via element.dataset.attributeName.
Attributes starting with aria- are for accessibility. For example, aria-label="Close" tells screen readers what a button does when it only has an icon and no visible text. The hidden attribute hides elements from both visual display AND screen readers.
CSS works by selecting HTML elements and applying style rules to them. A CSS rule has two parts: a selector (what to style) and declarations (how to style it). For example: .button { color: blue; } selects all elements with the class "button" and makes their text blue.
At the top of base.css, you'll see a block starting with :root. This is where we define CSS custom properties, also known as CSS variables. They look like --color-primary: #6366f1;.
The :root selector targets the root element (the <html> tag), making these variables available throughout the entire document. We use them with the var() function: color: var(--color-primary);.
Why use variables? If we want to change the primary color of the entire site, we only need to change it in one place. Without variables, we'd have to find and update every instance of that color value throughout our stylesheets.
Browsers have built-in default styles - paragraphs have margins, headings have specific sizes, lists have bullet points. Unfortunately, different browsers have slightly different defaults. The CSS reset at the top of base.css removes these inconsistencies:
Our CSS classes follow a pattern called BEM (Block, Element, Modifier). This naming convention helps organize styles and prevent conflicts:
This creates a clear hierarchy. When you see .day-card__header, you immediately know it's the header element inside a day-card block, not just any header on the page.
The app looks different on phones versus desktops. This is achieved through "media queries" - CSS rules that only apply at certain screen sizes. For example:
@media (min-width: 1200px) { ... } means "only apply these styles when the screen is at least 1200 pixels wide."
Our app uses a "mobile-first" approach: we write styles for small screens first, then add media queries to enhance the layout for larger screens. This generally results in cleaner, more maintainable CSS.
When elements change state (like when you hover over a button), we use transitions to make the change smooth rather than instant. The transition property specifies what should animate, how long it takes, and the easing function (how the animation accelerates/decelerates).
For example, transition: color 200ms ease; means "when the color changes, animate the change over 200 milliseconds with an ease timing function."
JavaScript makes websites interactive. When you click a button, switch views, or see content load dynamically, that's JavaScript at work. Our app uses modern JavaScript features (ES6+) and a modular architecture.
Traditional JavaScript puts everything in one global space, which can cause naming conflicts. ES6 modules solve this by letting files explicitly import and export functionality:
In HTML, we load a module with <script type="module" src="...">. The browser then follows the import chain, loading each required file.
Every application needs a starting point. Our app begins in app.js, which does several things when the page loads:
The init() function orchestrates all of this. It's called when the DOM (Document Object Model - the browser's representation of your HTML) is ready.
You'll see the async and await keywords in our code. These handle "asynchronous" operations - things that take time to complete, like loading data from a file.
async function loadData() { ... } marks a function as asynchronous. Inside it, await someOperation() pauses execution until that operation completes. This makes asynchronous code read like synchronous code, which is much easier to understand.
In browsers, window is the global object representing the browser window. We use it to:
Our app displays information about karaoke venues. This data is stored in data.js as a JavaScript object and managed through venues.js service.
The venue data is organized as a JavaScript object with a "listings" array. Each venue in the array has properties describing it:
Instead of accessing the raw data directly, components use functions from venues.js. This is called a "service layer" - it provides a clean interface for working with data:
This abstraction has several benefits: if we ever change how data is stored (maybe fetching from a server instead of a local file), we only need to update the service - all the components using it continue to work unchanged.
One of the trickier parts of the app is determining which venues have karaoke on a given date. The scheduleMatchesDate() function in date.js handles this. It considers:
For "first", "second", etc., it calculates which occurrence of that weekday in the month the date falls on. For "last", it checks if there's another occurrence of that weekday later in the same month. For "once" events, it simply compares the schedule's date field against the target date.
Venues can be tagged with characteristics that help users find the right spot. Tags are displayed as color-coded badges on venue cards throughout the app.
At the top of data.js, there's a tagDefinitions object that defines all available tags:
Each tag definition includes a human-readable label, background color, and text color for proper contrast.
The tags.js utility module handles tag rendering:
Tags are rendered using inline styles (background color and text color) from the tag configuration, wrapped in .venue-tags and .venue-tag CSS classes for layout and sizing.
Tags are displayed in all venue displays throughout the app:
Adding New Tags
To add a new tag, simply add it to the tagDefinitions object in data.js with a unique ID, label, background color, and text color. The system will automatically make it available throughout the app.
"State" refers to the data that can change as users interact with the app. In our case, this includes which view is active, what week is being displayed, whether dedicated venues are shown, and which venue (if any) is selected.
Without centralized state, components would need to directly communicate with each other, creating a tangled web of dependencies. With centralized state, components simply read from and write to one place. When state changes, the system notifies all interested components.
Our state is stored in a simple JavaScript object:
Components can "subscribe" to state changes using subscribe(key, callback). When that piece of state changes, the callback function is called with the new value. This is similar to how you might subscribe to a YouTube channel - you get notified when new content appears.
For example: subscribe('view', (newView) => this.render()) means "whenever the view state changes, re-render this component."
State is updated via setState({ key: value }). This function:
This is a "reactive" pattern - changes automatically propagate through the system. You don't need to manually tell every component "hey, the view changed" - it happens automatically.
While state management handles data changes, the event system handles actions and communication between components. Think of events as announcements: "A venue was selected!" or "The modal was closed!"
Our event bus is like a bulletin board where components can post messages and other components can listen for them. It provides several functions:
Imagine the VenueCard component needs to tell the VenueModal to open with specific venue details. Without events, VenueCard would need to know about VenueModal directly, creating a tight coupling. If we change or remove VenueModal, VenueCard breaks.
With events, VenueCard just announces emit(Events.VENUE_SELECTED, venue). It doesn't know or care who's listening. VenueModal (and VenueDetailPane, and anything else that cares) independently listens for that event. Components are decoupled - they can be added, removed, or changed without affecting each other.
The Events object defines all event names used in the app:
Design Pattern: Pub/Sub
This event system implements the Publisher/Subscriber (Pub/Sub) pattern. Publishers (emit) don't need to know about subscribers (on), and subscribers don't need to know about publishers. This loose coupling makes the code more flexible and easier to maintain.
Modern web development organizes UIs into "components" - reusable, self-contained pieces. Our app has a simple component system built without any framework.
All our UI components extend the Component class. This base class provides common functionality:
When you create a component with new Navigation('#navigation'), the constructor:
When render() is called:
This pattern separates concerns: template() focuses purely on generating HTML, while afterRender() handles interactivity.
When you call setState({ key: value }), the component automatically re-renders with the new state.
The delegate(event, selector, handler) method enables "event delegation" - a technique where a single event listener on a parent handles events from many children. Instead of adding click listeners to 50 venue cards, we add one listener to their container that checks if the clicked element matches '.venue-card'.
Event delegation is more efficient and automatically handles dynamically added elements.
When a component is removed, destroy() cleans up:
This prevents "memory leaks" - leftover handlers that waste memory and can cause bugs.
Renders the view tabs (Calendar, A-Z, Map), week navigation arrows, a global search bar with clear button, and the "Show dedicated venues" toggle. The search bar filters venues across all views by name, city, neighborhood, host, company, and tags. Subscribes to view and weekStart state to update when these change. Notably, search query changes do not cause a full Navigation re-render, which preserves keyboard focus in the search input.
Displays a venue in either "compact" mode (for day cards) or "full" mode (for alphabetical view). Shows venue name, time, location, color-coded tags, host info, and special event indicators. Handles click events to select the venue. Event URLs are rendered as external links.
A full-screen popup for venue details on mobile. Listens for VENUE_SELECTED events and opens. Handles closing via button click, backdrop click, or Escape key.
A sticky side panel for venue details on desktop (1200px+ screens). Similar to VenueModal but displayed inline instead of as an overlay.
Shows all venues for a specific day. Displays the day name, date, "Today" badge if applicable, venue count, and a list of VenueCards. Past days are collapsed by default but can be expanded by clicking the header. Days with no venues after search/filter are collapsed with a .day-card--empty class. Special events sort to the top of the day's listings.
Views are full-page components that display venue information in different formats. The app has three views that users can switch between.
The default view shows a 7-day calendar. Here's how it works:
The view subscribes to weekStart and showDedicated state. When users navigate weeks or toggle the dedicated filter, the view re-renders.
Days in the past get a day-card--past class which collapses them by default. Users can click the header to expand and see that day's venues. This keeps the interface focused on upcoming events.
Shows all venues sorted A-Z, grouped by first letter:
The "ignore articles" logic is in getSortableName() - it moves articles like "The", "A", "An" to the end for sorting purposes. So "The Common Interest" sorts under "C", not "T".
The map view uses an immersive full-screen mode that hides all page chrome (header, footer, navigation) and displays venues on an interactive map using Leaflet.js.
The floating venue card replaces the modal overlay, allowing users to see the venue's location on the map while viewing its details.
Note About Map Coordinates
Not all venues have coordinates in the data. The map shows how many venues are mapped and suggests using the editor to add coordinates for unmapped venues.
Utility functions are small, reusable pieces of code that don't belong to any specific component. They're organized by purpose in the utils folder.
Working with dates in JavaScript is notoriously tricky. Our date utilities handle common operations:
Functions for manipulating and formatting text:
Functions for building and validating URLs:
Shared rendering functions used by both VenueModal and VenueDetailPane to avoid code duplication:
Development tools activated by adding ?debug=1 to the URL or setting localStorage.setItem('debug', '1'). When enabled, venue cards display the reason they appear on a given date (e.g., "Every Friday", "First Saturday"), and a "Debug Mode" indicator appears in the corner of the page.
Why Escape HTML?
If venue data contained <script>alert('hacked')</script> and we rendered it directly, that script would execute. The escapeHtml() function converts < to < and > to > so it displays as text instead of executing. Always escape user-provided content!
Now let's trace through what happens when you perform common actions in the app.
Understanding data flow is key to understanding any application:
Data flows down (from state to components), while events flow up (from user interactions to state changes). This unidirectional flow makes the app predictable and easier to debug.
Congratulations!
You've just learned the fundamentals of how a modern JavaScript application works. The concepts here - components, state management, event systems, modules - are used in frameworks like React, Vue, and Angular. Understanding them at this fundamental level will make learning any framework much easier.