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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: exploretech.la Continuous Integration

# Verifies every pull request targeting master. Runs the same install, test,
# image-manifest check (via prebuild) and production build that deployment uses,
# without any access to deployment secrets.
on:
pull_request:
branches: [master]
workflow_dispatch:

# Pull request code is untrusted: the job only ever needs to read the checkout.
permissions:
contents: read

# Superseded runs for the same pull request (or dispatch ref) are cancelled.
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Check out the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Do not leave the workflow token in .git/config for build scripts.
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
cache: npm

- name: Install packages
run: npm ci

- name: Run the tests
run: npm test

# prebuild runs npm run images:check, so the generated image map is
# verified against scripts/image-sources.json before bundling.
- name: Build the site
run: npm run build
57 changes: 40 additions & 17 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,39 +1,62 @@
name: exploretech.la Continuous Deployment

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
# Publishes the production build to the gh-pages branch. Only pushes to master
# deploy; pull requests are verified by the continuous integration workflow.
on:
push:
branches: [master]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
# The workflow token is never used to publish (the deploy step uses
# DEPLOY_ACCESS_TOKEN), so read access to the checkout is enough.
permissions:
contents: read

# Never run two publishes against gh-pages at once, and never cancel a publish
# that is already in flight.
concurrency:
group: deploy-gh-pages
cancel-in-progress: false

jobs:
# This workflow contains a single job called "build"
deploy:
# The type of runner that the job will run on
runs-on: ubuntu-latest
timeout-minutes: 20

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# Checks out the repository under $GITHUB_WORKSPACE
- name: Check out the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The deploy step configures its own credentialed remote.
persist-credentials: false

# Installs node on the runner, giving access to the npm command
- uses: actions/setup-node@v1
# Installs the Node version pinned in .nvmrc, with an npm cache
- name: Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
cache: npm

- name: Install packages
run: npm ci

# Runs the deploy script
- name: Run the deploy script
- name: Run the tests
run: npm test

- name: Build the production site
env:
VITE_GOOGLE_ANALYTICS_TRACKING_ID: ${{ secrets.GOOGLE_ANALYTICS_TRACKING_ID }}
run: npm run build

# Publish the verified output without running build tools with the token.
- name: Publish to gh-pages
env:
USER_NAME: "exploretech.la"
USER_EMAIL: "exploretechla@cs.ucla.edu"
REPOSITORY: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.DEPLOY_ACCESS_TOKEN }}
REACT_APP_GOOGLE_ANALYTICS_TRACKING_ID: ${{ secrets.GOOGLE_ANALYTICS_TRACKING_ID }}
run: |
git config --global user.name $USER_NAME
git config --global user.email $USER_EMAIL
git remote set-url origin https://${GITHUB_TOKEN}@github.com/${REPOSITORY}
npm run deploy
git config --global user.name "$USER_NAME"
git config --global user.email "$USER_EMAIL"
git remote set-url origin "https://${GITHUB_TOKEN}@github.com/${REPOSITORY}"
./node_modules/.bin/gh-pages -d build
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# misc
.DS_Store
.env
.env.*
!.env.example
npm-debug.log*
yarn-debug.log*
yarn-error.log*
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.21.0
135 changes: 50 additions & 85 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,58 @@
# exploretech.la

The website for [exploretech.la](https://www.exploretech.la/)
The React website for [exploretech.la](https://www.exploretech.la/), built with Vite and Dart Sass and hosted on GitHub Pages.

---
## Development

This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
Use **Node 24.21.0**, pinned in `.nvmrc` and shared with GitHub Actions.

You can find the most recent version of the React Guide [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md).
```sh
nvm install
nvm use
npm ci
npm start
```

The development server runs at `http://127.0.0.1:3000`. It fails clearly if that port is already occupied. JSX and stylesheet edits update through Vite's development server.

React 16.13 uses classic JSX in both application transforms and dependency scanning. The development config removes the plugin's automatic-runtime pre-bundles, which do not exist in that React version.

## Checks and production builds

Use Node **14.16.0** for installation, development, and builds. The existing Create React App and node-sass versions do not support current Node releases.
```sh
npm test # Run the regression suite once
npm run test:watch # Watch tests while developing
npm run images:check # Check generated image integrity and budgets
npm run build # Check images, then build into build/
npm run preview # Serve the production build locally
```

Tests use Vitest and jsdom. Analytics tests use a dummy ID and make no provider requests.

The build/test tooling is modernized independently of the UI libraries. React 16, React Router 5, and Bootstrap 4 remain pinned to their previously deployed versions. Dart Sass currently warns about legacy imports and Bootstrap APIs; those warnings remain visible and belong to a subsequent stylesheet/UI-library upgrade.

## Updating images

Optimized images are checked in, so normal installs and builds need only Node.
To regenerate them, install `cwebp`, `ffmpeg`, and `ffprobe`. On macOS:
Optimized images are checked in. Normal installs, tests, CI, and builds do not need native image-generation tools.

To regenerate images, install `cwebp`, `ffmpeg`, and `ffprobe`. On macOS:

```sh
brew install webp ffmpeg
```

1. Keep the original image under `src/static/`.
2. Add its path and profile to `scripts/image-sources.json`. Portraits use a square crop up to 320px for the 160px team cards. Content images get responsive widths without enlargement. Use an optional `focus` override when the default crop cuts a face.
3. Run `npm run images`. This applies EXIF orientation, generates WebP files, and updates the manifest and `src/constants/optimizedImages.js`.
4. Use the generated map instead of importing an original photograph:
2. Add its path and profile to `scripts/image-sources.json`. Portraits use a square crop up to 320px for the 160px team cards. Content images get responsive widths without enlargement. Use a `focus` override when the default crop cuts a face.
3. Run `npm run images`. It applies EXIF orientation, generates WebP files, and updates the manifest and `src/constants/optimizedImages.js`.
4. Import the generated map rather than an original photograph:

```jsx
import images from "constants/optimizedImages";

// Team roster entries retain a plain image URL.
const portrait = images["team/leadership/sandra-pan.jpg"].src;

// Content images also carry responsive sources and intrinsic dimensions.
// Content images carry responsive sources and intrinsic dimensions.
<img
{...images["images/explore-tech-2022.jpg"]}
sizes="(min-width: 768px) 480px, 100vw"
Expand All @@ -42,85 +64,28 @@ const portrait = images["team/leadership/sandra-pan.jpg"].src;

5. Run `npm run images:check`, then inspect the affected pages at phone and desktop widths. Check faces, transparent logos, and images revealed by scrolling.

`npm run build` runs the image check automatically. It checks source and output hashes, recipe freshness, dimensions, byte budgets, generated-map consistency, and raw raster imports in the configured JavaScript sources. It needs no native image tools. Do not edit generated files by hand.
The check validates source/output hashes, recipe freshness, dimensions, byte budgets, generated-map consistency, and raw raster imports in the configured JavaScript sources. Do not edit generated files by hand.

Use `npm run images -- --only <path-fragment>` for a narrow update, or `npm run images -- --force` after changing encoder behavior or tools. Run `npm run images -- --help` for profile budgets and crop settings. Original files remain regeneration inputs; PDF and map links are excluded from this pipeline.

## Loading and deployment decisions

- Keep above-fold images eager. Offscreen portraits, photos, and sponsor logos use native lazy loading with reserved dimensions. The carousel keeps its current image until a requested slide loads.
- Archived videos make no YouTube requests until the visitor activates a play button.
- Internal routes use React Router. Hash navigation, mobile menu closure, and browser Back scroll restoration are handled without reloading the document.
- Analytics retains the configured tracker and connected GA4 tag. Commands queue immediately; the vendor loads after page load during idle time, on interaction, or after a four-second deadline. Development and builds without `REACT_APP_GOOGLE_ANALYTICS_TRACKING_ID` do not initialize analytics.
- Bootstrap is included once through `src/App.scss`.
- GitHub Pages currently serves hashed assets with a ten-minute cache lifetime. React cannot change those HTTP headers. Longer immutable caching requires a separate hosting/CDN decision; adding a `_headers` file here would not configure GitHub Pages.
- Route imports remain static. The measured first-party JavaScript was about 100 KB compressed; image delivery was the dominant cost. Reconsider route splitting if future measurements justify additional chunk-loading behavior.

To verify changes locally, run `npm test -- --watchAll=false --runInBand` and `npm run build`. The analytics tests use a dummy ID and make no provider requests.

## Available Scripts

In the project directory, you can run:

### `npm start`

Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.

The page will reload when you make changes.\
You may also see any lint errors in the console.

### `npm test`

Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.

### `npm run build`

Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.

### `npm run eject`

**Note: this is a one-way operation. Once you `eject`, you can't go back!**

If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.

Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.

You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.

## Learn More

You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).

To learn React, check out the [React documentation](https://reactjs.org/).

### Code Splitting

This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)

### Analyzing the Bundle Size

This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)

### Making a Progressive Web App

This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)

### Advanced Configuration
## CI and deployment

This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
- Pull requests targeting **`master`** run `.github/workflows/ci.yml`: `npm ci`, `npm test`, and `npm run build`, including its image checks. This job has read-only repository access, receives no deployment secrets, and never publishes the site.
- Pushes to **`master`** run `.github/workflows/deploy.yml`: install, test, build, then publish the verified `build/` directory to **`gh-pages`**. The deploy token is available only to the publishing step, not to installation, tests, or build tools.
- Both workflows use the Node version in `.nvmrc`, the committed lockfile, npm caching, and SHA-pinned official checkout/setup actions.
- The existing `DEPLOY_ACCESS_TOKEN` and `GOOGLE_ANALYTICS_TRACKING_ID` repository secret names are unchanged. Only the build-time environment variable is renamed to `VITE_GOOGLE_ANALYTICS_TRACKING_ID`.
- Branch protection and review requirements are unchanged. The PR check is named `verify`; an administrator can make it required in branch protection if desired.

### Deployment
`npm run deploy` remains available for an authorized manual release and builds before publishing. Do not run it just to preview changes.

This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
## Runtime and hosting conventions

### `npm run build` fails to minify
- Root `index.html` is the Vite entry. It retains the GitHub Pages query-to-route restoration script; `public/404.html` and `public/CNAME` are copied unchanged.
- Public media retain the existing `static/media/<name>.<content-hash>.<extension>` URLs so shared PDF/map links and cached images survive the bundler migration. JavaScript and CSS filenames may change.
- `components/`, `constants/`, `static/`, and `util/` imports resolve from `src/`; `jsconfig.json` keeps the same editor lookup root.
- `VITE_GOOGLE_ANALYTICS_TRACKING_ID` is optional and is used only in production builds. Vite-prefixed variables are public client configuration, not a place for private credentials. Analytics keeps the existing tracker and connected GA4 behavior, queues early commands, and defers vendor loading until idle, interaction, or its deadline.
- Fonts load from an HTML stylesheet link rather than a nested CSS import, preventing an unstyled startup transition in WebKit.
- Initially visible images stay eager; offscreen content uses lazy loading with reserved dimensions. The carousel waits for selected images and skips failures. Archived videos load only after activation.
- GitHub Pages' cache headers and the hosting configuration are unchanged. Longer immutable caching would require a separate hosting/CDN decision.

This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
npm install-script decisions are version-scoped in `package.json`. The optional watcher source-build hooks are denied; supported platforms use their prebuilt packages. Review `npm install-scripts ls` before changing these decisions for a dependency update.
11 changes: 8 additions & 3 deletions public/index.html → index.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="shortcut icon" href="/favicon.ico" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&amp;display=swap"
/>
<title>exploretech.la</title>

<script type="text/javascript">
Expand All @@ -29,13 +33,14 @@
window.history.replaceState(
null,
null,
l.pathname.slice(0, -1) + decoded + l.hash
l.pathname.slice(0, -1) + decoded + l.hash,
);
}
})(window.location);
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
Loading