diff --git a/.babelrc b/.babelrc index 00c73b2..1ff94f7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,3 @@ { - "presets": ["next/babel"], - "plugins": ["@vanilla-extract/babel-plugin"] + "presets": ["next/babel"] } diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 538ae09..9256f09 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,15 +17,16 @@ jobs: - uses: actions/checkout@v2 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: - node-version: '14.x' + node-version: '20' + cache: 'yarn' - name: Install dependencies - run: yarn --immutable + run: yarn --frozen-lockfile - - name: Tests - run: yarn test + - name: Type check + run: yarn lint:types - name: Build Docker image run: docker build . --tag $IMAGE_NAME diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c22fe44 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,57 @@ +name: E2E Tests + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + e2e-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and start services + run: | + docker compose -f docker-compose.test.yml up -d --build + + - name: Wait for service to be healthy + run: | + echo "Waiting for dave service to be healthy..." + timeout 60 bash -c 'until docker compose -f docker-compose.test.yml ps dave | grep -q "healthy"; do sleep 2; done' + echo "Service is healthy!" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Install Playwright browsers + run: npx playwright install chromium --with-deps + + - name: Run Playwright tests + run: npx playwright test + env: + BASE_URL: http://localhost:3000 + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Stop services + if: always() + run: docker compose -f docker-compose.test.yml down diff --git a/.gitignore b/.gitignore index fffa5bd..d2eba27 100644 --- a/.gitignore +++ b/.gitignore @@ -211,3 +211,8 @@ $RECYCLE.BIN/ # .pnp.* # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,node,yarn + +# Playwright +playwright-report/ +playwright/.cache/ +test-results/ diff --git a/.node-version b/.node-version index 958b5a3..9a2a0e2 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v14 +v20 diff --git a/Dockerfile b/Dockerfile index 9871544..17704d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,22 @@ # Stage 1 - Build -FROM node:14 AS builder +FROM node:20 AS builder WORKDIR /app COPY package.json yarn.lock /app/ -RUN yarn --frosen-lockfile +RUN yarn --frozen-lockfile COPY . /app/ -RUN NEXT_TELEMETRY_DISABLED=1 yarn next build +RUN NODE_OPTIONS=--openssl-legacy-provider NEXT_TELEMETRY_DISABLED=1 yarn next build # Stage 2 - Running the app -FROM node:14-alpine +FROM node:20-alpine WORKDIR /app -ENV NODE_ENV production +ENV NODE_ENV=production -# You only need to copy next.config.js if you are NOT using the default configuration - COPY --from=builder /app/next.config.js ./ -#COPY --from=builder /app/public ./public +COPY --from=builder /app/next.config.js ./ COPY --from=builder /app/.next ./.next COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..e5fba70 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + dave: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:80" + environment: + - NODE_ENV=production + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 10s diff --git a/e2e/app.spec.ts b/e2e/app.spec.ts new file mode 100644 index 0000000..a5fb02c --- /dev/null +++ b/e2e/app.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Dave Dashboard', () => { + test('should load the homepage successfully', async ({ page }) => { + const response = await page.goto('/', { waitUntil: 'domcontentloaded' }); + + // Log status for debugging + console.log('Response status:', response?.status()); + + // Check that the page loads (any 2xx status is OK) + expect(response?.ok()).toBe(true); + }); + + test('should render a page with HTML content', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded' }); + + // Check that there's an HTML element + const html = page.locator('html'); + await expect(html).toBeVisible(); + + // Log page content for debugging + const content = await page.content(); + console.log('Page content length:', content.length); + }); + + test('should have a body element with styles', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded' }); + + const body = page.locator('body'); + await expect(body).toBeVisible(); + + // Check that some CSS is applied + const bgColor = await body.evaluate((el) => + getComputedStyle(el).backgroundColor + ); + console.log('Background color:', bgColor); + + // Any background color that's not completely transparent is fine + expect(bgColor).toBeDefined(); + }); +}); diff --git a/next.config.js b/next.config.js index d6679ef..adb7f28 100644 --- a/next.config.js +++ b/next.config.js @@ -1,70 +1,16 @@ -const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin'); -const { getGlobalCssLoader } = require('next/dist/build/webpack/config/blocks/css/loaders'); -const MiniCssExtractPlugin = require('next/dist/build/webpack/plugins/mini-css-extract-plugin/src').default; - module.exports = { future: { webpack5: true, }, - webpack: (config, { dev, isServer, ...options }) => { + webpack: (config, { isServer }) => { // Fixes npm packages that depend on `fs` module if (!isServer) { config.resolve.fallback = { - fs: false, + fs: false, path: require.resolve('path-browserify'), }; } - // --- vanilla-extract config --- - // based on: https://github.com/seek-oss/vanilla-extract/issues/4#issuecomment-810842869 - config.module.rules.push({ - test: /\.css$/i, - sideEffects: true, - use: dev - ? getGlobalCssLoader( - { - assetPrefix: options.config.assetPrefix, - future: { - webpack5: true, - }, - isClient: !isServer, - isServer, - isDevelopment: dev, - }, - [], - [] - ) - : [MiniCssExtractPlugin.loader, 'css-loader'], - }); - - config.plugins.push( - new VanillaExtractPlugin() - ); - - if (!dev) { - config.plugins.push( - new MiniCssExtractPlugin({ - filename: 'static/css/[contenthash].css', - chunkFilename: 'static/css/[contenthash].css', - ignoreOrder: true, - }) - ); - } - - config.resolve.extensions = ['.js', '.jsx', '.ts', '.tsx', '.css', '.css.ts']; - // ------------------------------ - - // - Fonts - - - config.module.rules.push({ - test: /\.(woff|woff2|eot|ttf)$/, - loader: 'file-loader', - options: { - outputPath: 'static/font/', - publicPath: '/_next/static/font/' - } - }); - return config; }, }; diff --git a/package.json b/package.json index ef87c43..4c7aeaa 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dev": "next dev", "build": "next build", "test": "jest", + "test:e2e": "playwright test", "lint:types": "tsc --noEmit" }, "dependencies": { @@ -21,22 +22,13 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@playwright/test": "^1.58.0", "@testing-library/react": "^11.2.6", "@testing-library/react-hooks": "^5.1.2", "@types/jest": "^26.0.22", - "@types/react": "^17.0.3", - "@types/whatwg-url": "^8.2.0", - "@vanilla-extract/babel-plugin": "^0.1.0", - "@vanilla-extract/css": "^0.1.0", - "@vanilla-extract/dynamic": "^0.1.0", - "@vanilla-extract/webpack-plugin": "^0.1.0", - "css-loader": "^5.2.0", - "file-loader": "^6.2.0", + "@types/node": "^14.0.0", + "@types/react": "17.0.2", "jest": "^26.6.3", - "typescript": "^4.2.3", - "url-loader": "^4.1.1" - }, - "resolutions": { - "webpack": "5.29.0" + "typescript": "^4.2.3" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..264e981 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: process.env.BASE_URL || 'http://localhost:3000', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/src/cmdk/cmdk.css.ts b/src/cmdk/cmdk.css.ts deleted file mode 100644 index d80743a..0000000 --- a/src/cmdk/cmdk.css.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - createThemeContract, - createTheme, - style, - globalStyle, -} from '@vanilla-extract/css'; - -// Inspired by https://github.com/pacocoursey/cmdk/blob/main/website/styles/cmdk/vercel.scss - -const gray1 = 'hsl(0, 0%, 99%)'; -const grayA3 = 'hsla(0, 0%, 0%, 0.047)'; -const gray4 = 'hsl(0, 0%, 93%)'; -const gray5 = 'hsl(0, 0%, 90.9%)'; -const gray6 = 'hsl(0, 0%, 88.7%)'; -const gray8 = 'hsl(0, 0%, 78%)'; -const gray9 = 'hsl(0, 0%, 56.1%)'; -const gray11 = 'hsl(0, 0%, 43.5%)'; -const gray12 = 'hsl(0, 0%, 9%)'; - -const fontSans = `'Inter', --apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif`; -const cmdkShadow = `0 16px 70px rgb(0 0 0 / 20%)`; -const appBg = gray1; - -export const cmdk = style({ - position: 'fixed', - zIndex: 100, - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', -}); - -globalStyle(`${cmdk} [cmdk-root]`, { - maxWidth: '640px', - width: '100%', - padding: '8px', - background: '#ffffff', - borderRadius: '12px', - overflow: 'hidden', - fontFamily: fontSans, - border: `1px solid ${gray6}`, - boxShadow: cmdkShadow, - transition: 'transform 100ms ease', -}); - -globalStyle(`${cmdk} [cmdk-root] .dark &`, { - background: 'rgba(22, 22, 22, 0.7)', -}); - -globalStyle(`${cmdk} [cmdk-input]`, { - fontFamily: fontSans, - border: 'none', - width: '100%', - fontSize: '17px', - padding: '8px 8px 16px 8px', - outline: 'none', - background: appBg, - color: gray12, - borderBottom: `1px solid ${gray6}`, - marginBottom: '16px', - borderRadius: '0', -}); - -globalStyle(`${cmdk} [cmdk-input]::placeholder`, { - color: gray9, -}); - -globalStyle(`${cmdk} [cmdk-item]`, { - contentVisibility: 'auto', - cursor: 'pointer', - height: '48px', - borderRadius: '8px', - fontSize: '14px', - display: 'flex', - alignItems: 'center', - gap: '8px', - padding: '0 16px', - color: gray11, - userSelect: 'none', - willChange: 'background, color', - transition: 'all 150ms ease', - transitionProperty: 'none', -}); - -globalStyle(`${cmdk} [cmdk-item][aria-selected='true']`, { - background: grayA3, - color: gray12, -}); - -globalStyle(`${cmdk} [cmdk-item][aria-disabled='true']`, { - color: gray8, - cursor: 'not-allowed', -}); - -globalStyle(`${cmdk} [cmdk-item]:active`, { - transitionProperty: 'background', - background: gray4, -}); - -globalStyle(`${cmdk} [cmdk-item] + [cmdk-item]`, { - marginTop: '4px', -}); - -globalStyle(`${cmdk} [cmdk-item] svg`, { - width: '18px', - height: '18px', -}); - -globalStyle(`${cmdk} [cmdk-list]`, { - height: 'min(330px, calc(var(--cmdk-list-height)))', - maxHeight: '400px', - overflow: 'auto', - overscrollBehavior: 'contain', - transition: '100ms ease', - transitionProperty: 'height', -}); - -globalStyle(`${cmdk} [cmdk-vercel-shortcuts]`, { - display: 'flex', - marginLeft: 'auto', - gap: '8px', -}); - -globalStyle(`${cmdk} [cmdk-vercel-shortcuts] kbd`, { - fontFamily: fontSans, - fontSize: '12px', - minWidth: '20px', - padding: '4px', - height: '20px', - borderRadius: '4px', - color: gray11, - background: gray4, - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - textTransform: 'uppercase', -}); - -globalStyle(`${cmdk} [cmdk-separator]`, { - height: '1px', - width: '100%', - background: gray5, - margin: '4px 0', -}); - -globalStyle(`${cmdk} *:not([hidden]) + [cmdk-group]`, { - marginTop: '8px', -}); - -globalStyle(`${cmdk} [cmdk-group-heading]`, { - userSelect: 'none', - fontSize: '12px', - color: gray11, - padding: '0 8px', - display: 'flex', - alignItems: 'center', - marginBottom: '8px', -}); - -globalStyle(`${cmdk} [cmdk-empty]`, { - fontSize: '14px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: '48px', - whiteSpace: 'pre-wrap', - color: gray11, -}); diff --git a/src/cmdk/cmdk.module.css b/src/cmdk/cmdk.module.css new file mode 100644 index 0000000..5434410 --- /dev/null +++ b/src/cmdk/cmdk.module.css @@ -0,0 +1,140 @@ +.cmdk { + position: fixed; + z-index: 100; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.cmdk [cmdk-root] { + max-width: 640px; + width: 100%; + padding: 8px; + background: #ffffff; + border-radius: 12px; + overflow: hidden; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + border: 1px solid hsl(0, 0%, 88.7%); + box-shadow: 0 16px 70px rgb(0 0 0 / 20%); + transition: transform 100ms ease; +} + +.cmdk [cmdk-input] { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + border: none; + width: 100%; + font-size: 17px; + padding: 8px 8px 16px 8px; + outline: none; + background: hsl(0, 0%, 99%); + color: hsl(0, 0%, 9%); + border-bottom: 1px solid hsl(0, 0%, 88.7%); + margin-bottom: 16px; + border-radius: 0; +} + +.cmdk [cmdk-input]::placeholder { + color: hsl(0, 0%, 56.1%); +} + +.cmdk [cmdk-item] { + content-visibility: auto; + cursor: pointer; + height: 48px; + border-radius: 8px; + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 16px; + color: hsl(0, 0%, 43.5%); + user-select: none; + will-change: background, color; + transition: all 150ms ease; + transition-property: none; +} + +.cmdk [cmdk-item][aria-selected='true'] { + background: hsla(0, 0%, 0%, 0.047); + color: hsl(0, 0%, 9%); +} + +.cmdk [cmdk-item][aria-disabled='true'] { + color: hsl(0, 0%, 78%); + cursor: not-allowed; +} + +.cmdk [cmdk-item]:active { + transition-property: background; + background: hsl(0, 0%, 93%); +} + +.cmdk [cmdk-item] + [cmdk-item] { + margin-top: 4px; +} + +.cmdk [cmdk-item] svg { + width: 18px; + height: 18px; +} + +.cmdk [cmdk-list] { + height: min(330px, calc(var(--cmdk-list-height))); + max-height: 400px; + overflow: auto; + overscroll-behavior: contain; + transition: 100ms ease; + transition-property: height; +} + +.cmdk [cmdk-vercel-shortcuts] { + display: flex; + margin-left: auto; + gap: 8px; +} + +.cmdk [cmdk-vercel-shortcuts] kbd { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + font-size: 12px; + min-width: 20px; + padding: 4px; + height: 20px; + border-radius: 4px; + color: hsl(0, 0%, 43.5%); + background: hsl(0, 0%, 93%); + display: inline-flex; + align-items: center; + justify-content: center; + text-transform: uppercase; +} + +.cmdk [cmdk-separator] { + height: 1px; + width: 100%; + background: hsl(0, 0%, 90.9%); + margin: 4px 0; +} + +.cmdk *:not([hidden]) + [cmdk-group] { + margin-top: 8px; +} + +.cmdk [cmdk-group-heading] { + user-select: none; + font-size: 12px; + color: hsl(0, 0%, 43.5%); + padding: 0 8px; + display: flex; + align-items: center; + margin-bottom: 8px; +} + +.cmdk [cmdk-empty] { + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + height: 48px; + white-space: pre-wrap; + color: hsl(0, 0%, 43.5%); +} diff --git a/src/cmdk/index.jsx b/src/cmdk/index.jsx index 3b44a37..65f5afb 100644 --- a/src/cmdk/index.jsx +++ b/src/cmdk/index.jsx @@ -1,39 +1,40 @@ import React, { useState, useEffect, useRef } from 'react'; import { Command } from 'cmdk'; -import { cmdk } from './cmdk.css.ts'; +import styles from './cmdk.module.css'; const CommandMenu = ({ children }) => { - const [open, setOpen] = useState(false); - const containerElement = useRef(null) + const [open, setOpen] = useState(false); + const containerElement = useRef(null); - // Toggle the menu when ⌘K is pressed - useEffect(() => { - const down = (e) => { - if (e.key === 'k' && e.metaKey) { - e.preventDefault() - setOpen((open) => !open); + // Toggle the menu when ⌘K is pressed + useEffect(() => { + const down = (e) => { + if (e.key === 'k' && e.metaKey) { + e.preventDefault(); + setOpen((open) => !open); } - } + }; - document.addEventListener('keydown', down); - return () => document.removeEventListener('keydown', down); - }, []) + document.addEventListener('keydown', down); + return () => document.removeEventListener('keydown', down); + }, []); const onKeyDown = (e) => { - if(e.keyCode === 13 /* enter */) { + if (e.keyCode === 13 /* enter */) { setOpen(false); } - } + }; - return ( + return ( <> -
+
+ container={containerElement.current} + > No results found. @@ -41,7 +42,7 @@ const CommandMenu = ({ children }) => { - ); + ); }; export default CommandMenu; diff --git a/src/components/app/index.tsx b/src/components/app/index.tsx index ad9ae82..0bc7cbc 100644 --- a/src/components/app/index.tsx +++ b/src/components/app/index.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from 'react'; -import { className, appIconWrapper, appIcon, appStatus, appUrl, appA } from './style.css'; +import React from 'react'; +import styles from './style.module.css'; import { Icon } from '@iconify/react-with-api'; export interface AppProps { @@ -10,19 +10,21 @@ export interface AppProps { href: string; } -const App : React.FC = ({ id, icon, name, status, href }) => { - return
- -
- -
-
-
{name}
-
{status}
-
{href}
-
-
-
; -} +const App: React.FC = ({ id, icon, name, status, href }) => { + return ( +
+ +
+ +
+
+
{name}
+
{status}
+
{href}
+
+
+
+ ); +}; export default App; diff --git a/src/components/app/style.css.ts b/src/components/app/style.css.ts deleted file mode 100644 index 7ce596c..0000000 --- a/src/components/app/style.css.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { themeVars } from '../../styles/index.css'; - -export const className = style({ - display: 'flex', - ':hover': { - textDecoration: 'underline', - }, -}); - -export const appA = style({ - display: 'flex', - flexDirection: 'row', - textDecoration: 'none', - color: 'inherit', -}); - -export const appUrl = style({ - color: themeVars.color.accent, - fontSize: '0.8em', - fontStyle: 'italic', -}); - -export const appStatus = style({ - fontSize: '0.8em', - fontStyle: 'italic', -}); - -export const appIconWrapper = style({ - height: '3em', - width: '3em', - marginRight: '1em', -}); - -export const appIcon = style({ - height: '100%', - width: '100%', -}); diff --git a/src/components/app/style.module.css b/src/components/app/style.module.css new file mode 100644 index 0000000..20947b4 --- /dev/null +++ b/src/components/app/style.module.css @@ -0,0 +1,36 @@ +.container { + display: flex; +} + +.container:hover { + text-decoration: underline; +} + +.link { + display: flex; + flex-direction: row; + text-decoration: none; + color: inherit; +} + +.url { + color: var(--color-accent); + font-size: 0.8em; + font-style: italic; +} + +.status { + font-size: 0.8em; + font-style: italic; +} + +.iconWrapper { + height: 3em; + width: 3em; + margin-right: 1em; +} + +.icon { + height: 100%; + width: 100%; +} diff --git a/src/components/grid/index.tsx b/src/components/grid/index.tsx index 1addac5..7c63fd3 100644 --- a/src/components/grid/index.tsx +++ b/src/components/grid/index.tsx @@ -1,23 +1,22 @@ import React from 'react'; -import App, { AppProps } from '../app'; -import { themeClass, themeVars, className } from './style.css'; -import { createInlineTheme } from '@vanilla-extract/dynamic'; +import styles from './style.module.css'; interface Props { /* setting the min width for columns */ minWidth?: string; + children?: React.ReactNode; } -const Grid : React.FC = ({ children, minWidth='330px' }) => { - const customTheme = createInlineTheme(themeVars, { - minWidth, - }); +const Grid: React.FC = ({ children, minWidth = '330px' }) => { + const customStyle = { + '--grid-min-width': minWidth, + } as React.CSSProperties; - return
- { - children - } -
; -} + return ( +
+ {children} +
+ ); +}; export default Grid; diff --git a/src/components/grid/style.css.ts b/src/components/grid/style.css.ts deleted file mode 100644 index ed2eac1..0000000 --- a/src/components/grid/style.css.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createTheme, style } from '@vanilla-extract/css'; - -export const [themeClass, themeVars] = createTheme({ - minWidth: '330px', -}); - -export const className = style({ - display: 'grid', - gridTemplateColumns: `repeat(auto-fill, minmax(${themeVars.minWidth}, 1fr))`, - rowGap: '1.5em', - columnGap: '1.2em', -}); diff --git a/src/components/grid/style.module.css b/src/components/grid/style.module.css new file mode 100644 index 0000000..2659d21 --- /dev/null +++ b/src/components/grid/style.module.css @@ -0,0 +1,6 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--grid-min-width, 330px), 1fr)); + row-gap: 1.5em; + column-gap: 1.2em; +} diff --git a/src/docker/index.ts b/src/docker/index.ts index bbda0af..d9f994e 100644 --- a/src/docker/index.ts +++ b/src/docker/index.ts @@ -78,9 +78,14 @@ const processContainer = (containers : Container[]) : AppProps[] => { .sort((first, second) => first.name.localeCompare(second.name)); }; -export const getContainersWithLabels = async () => { - const docker = new Docker({ socketPath: '/var/run/docker.sock' }); - const containers = await fetch(docker, '/containers/json') as Container[]; - - return processContainer(containers); +export const getContainersWithLabels = async (): Promise => { + try { + const docker = new Docker({ socketPath: '/var/run/docker.sock' }); + const containers = await fetch(docker, '/containers/json') as Container[]; + return processContainer(containers); + } catch (error) { + // Docker socket not available - return empty list + console.warn('Docker socket not available, returning empty app list:', error); + return []; + } }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx new file mode 100644 index 0000000..8efcc44 --- /dev/null +++ b/src/pages/_app.tsx @@ -0,0 +1,9 @@ +import type { AppProps } from 'next/app'; +import '../styles/globals.css'; +import 'inter-ui/inter.css'; + +function MyApp({ Component, pageProps }: AppProps) { + return ; +} + +export default MyApp; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index fbc62b0..33260ff 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,13 +1,11 @@ import React, { useMemo } from 'react'; -import { GetServerSideProps } from 'next' +import { GetServerSideProps } from 'next'; import MDX from '@mdx-js/runtime'; import App, { AppProps as IApp } from '../components/app'; import Grid from '../components/grid'; import useForceHttps, { ForceHttpsStatus, replaceUrlWithHttps } from '../force-https'; import { getContainersWithLabels, AppProps } from '../docker'; -import { themeVars } from '../styles/index.css'; -import 'inter-ui/Inter (web)/inter.css'; -import { createInlineTheme } from '@vanilla-extract/dynamic'; +import { createInlineTheme } from '../styles/theme'; import CMDK from '../cmdk'; import { Command } from 'cmdk'; @@ -15,57 +13,59 @@ interface Colors { background?: string; text?: string; accent?: string; -}; +} interface Props { colors: Colors; mdx: string; appData: AppProps[]; forceHttps: string | boolean; -}; +} -const Style : React.FC<{ colors : Colors }> = ({ colors }) => { +const Style: React.FC<{ colors: Colors }> = ({ colors }) => { const style = useMemo(() => { - if(typeof window !== 'undefined') { - const match = window.location.hash.match(/^\#([0-9a-f]{3,6})\-([0-9a-f]{3,6})\-([0-9a-f]{3,6})$/i); - if(match !== null) { - const color = { + if (typeof window !== 'undefined') { + const match = window.location.hash.match( + /^\#([0-9a-f]{3,6})\-([0-9a-f]{3,6})\-([0-9a-f]{3,6})$/i + ); + if (match !== null) { + return createInlineTheme({ background: '#' + match[1], text: '#' + match[2], accent: '#' + match[3], - }; - - return createInlineTheme(themeVars, { color }); + }); } } - return createInlineTheme(themeVars, { color: colors as any }); - }, [ (typeof window == 'undefined' ? { location: { hash: '' }} : window ).location.hash ]) + return createInlineTheme(colors); + }, [typeof window == 'undefined' ? '' : window.location.hash]); + + const bgVar = style['--color-background']; + const textVar = style['--color-text']; + const accentVar = style['--color-accent']; return ( - - ); + + ); +}; -const Home : React.FC = ({ colors, mdx, appData, forceHttps }) => { +const Home: React.FC = ({ colors, mdx, appData, forceHttps }) => { const httpStatus = useForceHttps(forceHttps); - let apps : IApp[] = []; - - if(typeof window !== 'undefined') { - apps = (appData||[]).map(({ - relativeSubdomain, - url, - ...app - }) => { + let apps: IApp[] = []; + + if (typeof window !== 'undefined') { + apps = (appData || []).map(({ relativeSubdomain, url, ...app }) => { const href = relativeSubdomain ? `//${relativeSubdomain}.${window.location.host}/` - : (url || ""); + : url || ''; - if(httpStatus === ForceHttpsStatus.All) { + if (httpStatus === ForceHttpsStatus.All) { return { ...app, href: replaceUrlWithHttps(href) }; } @@ -73,27 +73,33 @@ const Home : React.FC = ({ colors, mdx, appData, forceHttps }) => { }); } - const gotoApp = (app) => { + const gotoApp = (app: IApp) => { window.location.href = app.href; }; - return <> - - { (apps || []).map((app) => ( - gotoApp(app)}>{ app.name } - )) } - - -