Modernize
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
data
|
||||
.env
|
||||
*.local
|
||||
*.log
|
||||
@@ -0,0 +1,25 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker build -t vertex:ci .
|
||||
@@ -26,3 +26,5 @@ dist-ssr
|
||||
.env
|
||||
|
||||
data/
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
+11
-20
@@ -1,25 +1,16 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
ARG VITE_OPEN_WEATHER_KEY
|
||||
ARG VITE_OPEN_WEATHER_LAT
|
||||
ARG VITE_OPEN_WEATHER_LON
|
||||
ARG VITE_UNSPLASH_ACCESS_KEY
|
||||
|
||||
ENV VITE_OPEN_WEATHER_KEY=$VITE_OPEN_WEATHER_KEY
|
||||
ENV VITE_OPEN_WEATHER_LAT=$VITE_OPEN_WEATHER_LAT
|
||||
ENV VITE_OPEN_WEATHER_LON=$VITE_OPEN_WEATHER_LON
|
||||
ENV VITE_UNSPLASH_ACCESS_KEY=$VITE_UNSPLASH_ACCESS_KEY
|
||||
FROM node:24-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json .
|
||||
RUN npm install
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY public ./public
|
||||
COPY src ./src
|
||||
COPY index.html .
|
||||
COPY tsconfig.json .
|
||||
COPY postcss.config.cjs .
|
||||
COPY vite.config.ts .
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.29-alpine
|
||||
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --chmod=755 docker/40-vertex-config.sh /docker-entrypoint.d/40-vertex-config.sh
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
# vertex
|
||||
|
||||
An extensible start page
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
```sh
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
The image is a static production build served by nginx. Configuration stays
|
||||
editable at runtime — no rebuild needed:
|
||||
|
||||
- **`data/apps.yml`, `data/search-providers.yml`** — mounted via the `./data`
|
||||
volume and fetched by the browser on load; edit and refresh.
|
||||
- **`VITE_*` environment variables** (`.env`, see `docker-compose.yml`) — read
|
||||
at container start by an entrypoint script that generates `/config.js`.
|
||||
Restart the container to apply changes.
|
||||
|
||||
| Variable | Purpose |
|
||||
| --- | --- |
|
||||
| `VITE_OPEN_WEATHER_KEY` | OpenWeather API key for the weather widget |
|
||||
| `VITE_OPEN_WEATHER_LAT` / `VITE_OPEN_WEATHER_LON` | Weather location |
|
||||
| `VITE_UNSPLASH_ACCESS_KEY` | Unsplash API key for background images |
|
||||
|
||||
Note: as with any client-only SPA, these keys are visible to the browser.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# Writes the runtime config consumed by src/lib/runtime-config.ts from the
|
||||
# container's VITE_* environment variables, so keys can be set or rotated at
|
||||
# container start without rebuilding the image.
|
||||
set -eu
|
||||
|
||||
CONFIG_FILE=/usr/share/nginx/html/config.js
|
||||
|
||||
{
|
||||
echo 'window.__VERTEX_CONFIG__ = {'
|
||||
env | grep '^VITE_' | while IFS='=' read -r key value; do
|
||||
escaped=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
||||
printf ' "%s": "%s",\n' "$key" "$escaped"
|
||||
done
|
||||
echo '};'
|
||||
} > "$CONFIG_FILE"
|
||||
@@ -0,0 +1,28 @@
|
||||
server {
|
||||
listen 3000;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/css application/javascript application/json image/svg+xml;
|
||||
|
||||
# Runtime-editable: entrypoint-generated env config and the mounted
|
||||
# data/ volume must never be served from cache.
|
||||
location = /config.js {
|
||||
add_header Cache-Control "no-store";
|
||||
}
|
||||
|
||||
location /data/ {
|
||||
alias /app/data/;
|
||||
add_header Cache-Control "no-store";
|
||||
}
|
||||
|
||||
# Content-hashed build output is safe to cache forever.
|
||||
location /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/config.js"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
+1614
-1567
File diff suppressed because it is too large
Load Diff
+27
-25
@@ -11,6 +11,9 @@
|
||||
],
|
||||
"author": "Georgi Gardev <georgi@gar.dev>",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.12"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
@@ -18,34 +21,33 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/core": "^7.16.3",
|
||||
"@mantine/hooks": "^7.16.3",
|
||||
"@mantine/modals": "^7.16.3",
|
||||
"@tabler/icons-react": "^3.30.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"jotai": "^2.11.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.1.5",
|
||||
"@mantine/core": "^7.17.8",
|
||||
"@mantine/hooks": "^7.17.8",
|
||||
"@tabler/icons-react": "^3.44.0",
|
||||
"date-fns": "^4.4.0",
|
||||
"jotai": "^2.20.1",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-router": "^7.18.1",
|
||||
"reset.css": "^2.0.2",
|
||||
"unsplash-js": "^7.0.19"
|
||||
"unsplash-js": "^7.0.20",
|
||||
"yaml": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.18",
|
||||
"globals": "^15.14.0",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.0.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.22.0",
|
||||
"vite": "^5.1.0"
|
||||
"typescript": "~5.9.0",
|
||||
"typescript-eslint": "^8.46.0",
|
||||
"vite": "^8.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Placeholder for development. In production the container entrypoint
|
||||
// overwrites this file with values from the VITE_* environment variables.
|
||||
window.__VERTEX_CONFIG__ = {};
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { loadYaml } from '~/lib/load-yaml';
|
||||
import { CategoryDefinition } from '~/types';
|
||||
|
||||
async function getCategories(): Promise<CategoryDefinition[]> {
|
||||
return await import('../../data/apps.yml').then((m) => m.default.categories);
|
||||
const data = await loadYaml<{ categories: CategoryDefinition[] }>('/data/apps.yml');
|
||||
return data?.categories ?? [];
|
||||
}
|
||||
|
||||
export function useCategories() {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { loadYaml } from '~/lib/load-yaml';
|
||||
import { SearchProvider } from '~/types';
|
||||
|
||||
async function getSearchProviders(): Promise<SearchProvider[]> {
|
||||
return await import('../../data/search-providers.yml').then((m) => m.default.providers);
|
||||
const data = await loadYaml<{ providers: SearchProvider[] }>('/data/search-providers.yml');
|
||||
return data?.providers ?? [];
|
||||
}
|
||||
|
||||
export function useSearchProviders() {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getRuntimeEnv } from '~/lib/runtime-config';
|
||||
import { WeatherData } from '~/types';
|
||||
|
||||
function fetchWeather(): Promise<WeatherData> {
|
||||
const lat = import.meta.env.VITE_OPEN_WEATHER_LAT;
|
||||
const lon = import.meta.env.VITE_OPEN_WEATHER_LON;
|
||||
const appId = import.meta.env.VITE_OPEN_WEATHER_KEY;
|
||||
const lat = getRuntimeEnv('VITE_OPEN_WEATHER_LAT');
|
||||
const lon = getRuntimeEnv('VITE_OPEN_WEATHER_LON');
|
||||
const appId = getRuntimeEnv('VITE_OPEN_WEATHER_KEY');
|
||||
|
||||
return fetch(
|
||||
`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${appId}&units=metric`
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { createApi } from 'unsplash-js';
|
||||
import { getRuntimeEnv } from '~/lib/runtime-config';
|
||||
|
||||
const UNSPLASH_ACCESS_KEY = import.meta.env.VITE_UNSPLASH_ACCESS_KEY;
|
||||
const UNSPLASH_ACCESS_KEY = getRuntimeEnv('VITE_UNSPLASH_ACCESS_KEY') ?? '';
|
||||
|
||||
const unsplashApi = createApi({ accessKey: UNSPLASH_ACCESS_KEY, fetch });
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export function useActiveCategory() {
|
||||
}
|
||||
|
||||
if (mode === 'temporary') {
|
||||
storeTimeout(setTimeout(() => setActiveCategory(null), 3000));
|
||||
storeTimeout(window.setTimeout(() => setActiveCategory(null), 3000));
|
||||
}
|
||||
},
|
||||
[timeout]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { parse } from 'yaml';
|
||||
|
||||
// Config YAML is fetched at runtime (served from the mounted data/ volume)
|
||||
// rather than imported at build time, so edits to data/*.yml show up on
|
||||
// refresh without rebuilding the image.
|
||||
export async function loadYaml<T>(path: string): Promise<T | null> {
|
||||
try {
|
||||
const res = await fetch(path);
|
||||
if (!res.ok) {
|
||||
console.error(`Failed to load ${path}: ${res.status} ${res.statusText}`);
|
||||
return null;
|
||||
}
|
||||
return parse(await res.text()) as T;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${path}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Runtime configuration for static production builds.
|
||||
//
|
||||
// In production the container entrypoint writes /config.js, which sets
|
||||
// window.__VERTEX_CONFIG__ from the container's VITE_* environment variables.
|
||||
// In development that file is empty, so values fall back to import.meta.env
|
||||
// (populated by Vite from .env). This keeps env vars changeable at container
|
||||
// start instead of being baked into the bundle at build time.
|
||||
declare global {
|
||||
interface Window {
|
||||
__VERTEX_CONFIG__?: Record<string, string | undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
export function getRuntimeEnv(key: string): string | undefined {
|
||||
return window.__VERTEX_CONFIG__?.[key] ?? import.meta.env[key];
|
||||
}
|
||||
@@ -33,14 +33,6 @@ export function IndexPage() {
|
||||
[searchTerm, shouldSearchLocally, filteredApps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
parent.addEventListener('keydown', handleKeypress);
|
||||
|
||||
return () => {
|
||||
parent.removeEventListener('keydown', handleKeypress);
|
||||
};
|
||||
});
|
||||
|
||||
function handleKeypress(e: KeyboardEvent) {
|
||||
switch (e.key) {
|
||||
case 'ArrowRight': {
|
||||
@@ -72,6 +64,14 @@ export function IndexPage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
parent.addEventListener('keydown', handleKeypress);
|
||||
|
||||
return () => {
|
||||
parent.removeEventListener('keydown', handleKeypress);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Layout
|
||||
isLoading={isInitialLoading}
|
||||
|
||||
+1
-4
@@ -13,10 +13,7 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["src/*"],
|
||||
},
|
||||
"types": [
|
||||
"@modyfi/vite-plugin-yaml/modules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,11 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import ViteYaml from '@modyfi/vite-plugin-yaml';
|
||||
import path from 'path';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), ViteYaml()],
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
|
||||
Reference in New Issue
Block a user