Merge pull request #178 from sambecker/git-meta

Add fork status to app configuration page
This commit is contained in:
Sam Becker 2025-01-31 22:27:37 -06:00 committed by GitHub
commit 8c5edaf893
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 555 additions and 77 deletions

47
__tests__/github.test.ts Normal file
View File

@ -0,0 +1,47 @@
import { getGitHubMetaWithFallback, getGitHubPublicFork } from '@/admin/github';
import { TEMPLATE_BASE_OWNER, TEMPLATE_BASE_REPO } from '@/site/config';
describe('GitHub', () => {
it('fetches base repo meta', async () => {
const meta = await getGitHubMetaWithFallback({
owner: TEMPLATE_BASE_OWNER,
repo: TEMPLATE_BASE_REPO,
});
expect(meta).toBeDefined();
expect(meta.url).toBeDefined();
expect(meta.isForkedFromBase).toEqual(false);
expect(meta.label).toBeDefined();
expect(meta.title).toBeDefined();
expect(meta.isBehind).toEqual(false);
expect(meta.isBaseRepo).toBe(true);
});
it('fetches fork meta', async () => {
const fork = await getGitHubPublicFork();
const metaFork = await getGitHubMetaWithFallback(fork);
expect(metaFork.isForkedFromBase).toEqual(true);
});
it('handles nonexistent repos', async () => {
const meta = await getGitHubMetaWithFallback({
owner: 'nonexistent',
repo: 'nonexistent',
});
expect(meta).toBeDefined();
expect(meta.url).toBeDefined();
expect(meta.isForkedFromBase).toEqual(false);
expect(meta.label).toEqual('Unknown');
expect(meta.title).toEqual('Unknown');
expect(meta.isBehind).toBeUndefined();
});
it('handles fetch errors', async () => {
const meta = await getGitHubMetaWithFallback({
owner: 'gibberish / / *',
repo: 'bad text for a url.com',
});
expect(meta).toBeDefined();
expect(meta.url).toBeDefined();
expect(meta.isForkedFromBase).toEqual(false);
expect(meta.label).toEqual('Unknown');
expect(meta.title).toEqual('Unknown');
expect(meta.isBehind).toBeUndefined();
});
});

View File

@ -1,5 +1,7 @@
import { htmlHasBrParagraphBreaks, safelyParseFormattedHtml } from '@/utility/html';
import { parameterize } from '@/utility/string';
import {
htmlHasBrParagraphBreaks,
safelyParseFormattedHtml,
} from '@/utility/html';
describe('HTML', () => {
it('safely parses', () => {

View File

@ -1,4 +1,5 @@
/* eslint-disable max-len */
import type { Config } from 'jest';
import nextJest from 'next/jest.js';
const createJestConfig = nextJest({
@ -7,12 +8,10 @@ const createJestConfig = nextJest({
});
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async

1
jest.setup.ts Normal file
View File

@ -0,0 +1 @@
import 'cross-fetch/polyfill';

View File

@ -47,6 +47,7 @@
"@next/bundle-analyzer": "15.1.6",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/jest": "^29.5.14",
@ -57,12 +58,14 @@
"@types/sanitize-html": "^2.13.0",
"autoprefixer": "10.4.20",
"clsx": "^2.1.1",
"cross-fetch": "^4.1.0",
"eslint": "9.18.0",
"eslint-config-next": "15.1.6",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "8.5.1",
"tailwindcss": "3.4.17",
"ts-node": "^10.9.2",
"typescript": "5.7.3"
}
}

209
pnpm-lock.yaml generated
View File

@ -113,16 +113,19 @@ importers:
version: 15.1.6
'@tailwindcss/container-queries':
specifier: ^0.1.1
version: 0.1.1(tailwindcss@3.4.17)
version: 0.1.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)))
'@tailwindcss/forms':
specifier: ^0.5.10
version: 0.5.10(tailwindcss@3.4.17)
version: 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)))
'@testing-library/dom':
specifier: ^10.4.0
version: 10.4.0
'@testing-library/jest-dom':
specifier: ^6.6.3
version: 6.6.3
'@testing-library/react':
specifier: ^16.2.0
version: 16.2.0(@testing-library/dom@10.1.0)(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@types/jest':
specifier: ^29.5.14
version: 29.5.14
@ -147,6 +150,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
cross-fetch:
specifier: ^4.1.0
version: 4.1.0
eslint:
specifier: 9.18.0
version: 9.18.0(jiti@1.21.7)
@ -155,7 +161,7 @@ importers:
version: 15.1.6(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3)
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@22.10.7)
version: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
jest-environment-jsdom:
specifier: ^29.7.0
version: 29.7.0
@ -164,7 +170,10 @@ importers:
version: 8.5.1
tailwindcss:
specifier: 3.4.17
version: 3.4.17
version: 3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.10.7)(typescript@5.7.3)
typescript:
specifier: 5.7.3
version: 5.7.3
@ -566,6 +575,10 @@ packages:
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@discoveryjs/json-ext@0.5.7':
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
@ -847,6 +860,9 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@next/bundle-analyzer@15.1.6':
resolution: {integrity: sha512-hGzQyDqJzFHcHNCyTqM3o05BpVq5tGnRODccZBVJDBf5Miv/26UJPMB0wh9L9j3ylgHC+0/v8BaBnBBek1rC6Q==}
@ -1487,8 +1503,8 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1'
'@testing-library/dom@10.1.0':
resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==}
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
'@testing-library/jest-dom@6.6.3':
@ -1514,6 +1530,18 @@ packages:
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
'@tsconfig/node10@1.0.11':
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
'@tsconfig/node12@1.0.11':
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
'@tsconfig/node14@1.0.3':
resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
'@tsconfig/node16@1.0.4':
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@ -1819,6 +1847,9 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@ -2112,6 +2143,12 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
cross-fetch@4.1.0:
resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==}
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@ -2250,6 +2287,10 @@ packages:
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
diff@4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
@ -3217,6 +3258,9 @@ packages:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
@ -3355,6 +3399,15 @@ packages:
sass:
optional: true
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
@ -4098,6 +4151,9 @@ packages:
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
engines: {node: '>=6'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@3.0.0:
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
engines: {node: '>=12'}
@ -4114,6 +4170,20 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
ts-node@10.9.2:
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
tsconfig-paths@3.15.0:
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
@ -4233,6 +4303,9 @@ packages:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
v8-to-istanbul@9.2.0:
resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==}
engines: {node: '>=10.12.0'}
@ -4255,6 +4328,9 @@ packages:
walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@ -4276,6 +4352,9 @@ packages:
resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==}
engines: {node: '>=12'}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@ -4371,6 +4450,10 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yn@3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@ -5121,6 +5204,10 @@ snapshots:
'@bcoe/v8-coverage@0.2.3': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@discoveryjs/json-ext@0.5.7': {}
'@emnapi/runtime@1.2.0':
@ -5305,7 +5392,7 @@ snapshots:
jest-util: 29.7.0
slash: 3.0.0
'@jest/core@29.7.0':
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))':
dependencies:
'@jest/console': 29.7.0
'@jest/reporters': 29.7.0
@ -5319,7 +5406,7 @@ snapshots:
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
jest-config: 29.7.0(@types/node@22.10.7)
jest-config: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
@ -5475,6 +5562,11 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping@0.3.9':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
'@next/bundle-analyzer@15.1.6':
dependencies:
webpack-bundle-analyzer: 4.10.1
@ -6169,16 +6261,16 @@ snapshots:
dependencies:
tslib: 2.8.1
'@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.17)':
'@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)))':
dependencies:
tailwindcss: 3.4.17
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
'@tailwindcss/forms@0.5.10(tailwindcss@3.4.17)':
'@tailwindcss/forms@0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)))':
dependencies:
mini-svg-data-uri: 1.4.4
tailwindcss: 3.4.17
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
'@testing-library/dom@10.1.0':
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.24.2
'@babel/runtime': 7.24.5
@ -6199,10 +6291,10 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
'@testing-library/react@16.2.0(@testing-library/dom@10.1.0)(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
'@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.24.5
'@testing-library/dom': 10.1.0
'@testing-library/dom': 10.4.0
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
@ -6211,6 +6303,14 @@ snapshots:
'@tootallnate/once@2.0.0': {}
'@tsconfig/node10@1.0.11': {}
'@tsconfig/node12@1.0.11': {}
'@tsconfig/node14@1.0.3': {}
'@tsconfig/node16@1.0.4': {}
'@types/aria-query@5.0.4': {}
'@types/babel__core@7.20.5':
@ -6552,6 +6652,8 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
arg@4.1.3: {}
arg@5.0.2: {}
argparse@1.0.10:
@ -6902,13 +7004,13 @@ snapshots:
cookie@0.7.1: {}
create-jest@29.7.0(@types/node@22.10.7):
create-jest@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@22.10.7)
jest-config: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
@ -6917,6 +7019,14 @@ snapshots:
- supports-color
- ts-node
create-require@1.1.1: {}
cross-fetch@4.1.0:
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
cross-spawn@7.0.3:
dependencies:
path-key: 3.1.1
@ -7029,6 +7139,8 @@ snapshots:
diff-sequences@29.6.3: {}
diff@4.0.2: {}
dlv@1.1.3: {}
doctrine@2.1.0:
@ -7926,16 +8038,16 @@ snapshots:
- babel-plugin-macros
- supports-color
jest-cli@29.7.0(@types/node@22.10.7):
jest-cli@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)):
dependencies:
'@jest/core': 29.7.0
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0(@types/node@22.10.7)
create-jest: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
exit: 0.1.2
import-local: 3.1.0
jest-config: 29.7.0(@types/node@22.10.7)
jest-config: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
@ -7945,7 +8057,7 @@ snapshots:
- supports-color
- ts-node
jest-config@29.7.0(@types/node@22.10.7):
jest-config@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)):
dependencies:
'@babel/core': 7.24.5
'@jest/test-sequencer': 29.7.0
@ -7971,6 +8083,7 @@ snapshots:
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 22.10.7
ts-node: 10.9.2(@types/node@22.10.7)(typescript@5.7.3)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@ -8205,12 +8318,12 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
jest@29.7.0(@types/node@22.10.7):
jest@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)):
dependencies:
'@jest/core': 29.7.0
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
'@jest/types': 29.6.3
import-local: 3.1.0
jest-cli: 29.7.0(@types/node@22.10.7)
jest-cli: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -8355,6 +8468,8 @@ snapshots:
dependencies:
semver: 7.6.3
make-error@1.3.6: {}
makeerror@1.0.12:
dependencies:
tmpl: 1.0.5
@ -8465,6 +8580,10 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-int64@0.4.0: {}
node-releases@2.0.14: {}
@ -8678,12 +8797,13 @@ snapshots:
camelcase-css: 2.0.1
postcss: 8.5.1
postcss-load-config@4.0.2(postcss@8.5.1):
postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)):
dependencies:
lilconfig: 3.1.3
yaml: 2.4.2
optionalDependencies:
postcss: 8.5.1
ts-node: 10.9.2(@types/node@22.10.7)(typescript@5.7.3)
postcss-nested@6.2.0(postcss@8.5.1):
dependencies:
@ -9194,7 +9314,7 @@ snapshots:
symbol-tree@3.2.4: {}
tailwindcss@3.4.17:
tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@ -9213,7 +9333,7 @@ snapshots:
postcss: 8.5.1
postcss-import: 15.1.0(postcss@8.5.1)
postcss-js: 4.0.1(postcss@8.5.1)
postcss-load-config: 4.0.2(postcss@8.5.1)
postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))
postcss-nested: 6.2.0(postcss@8.5.1)
postcss-selector-parser: 6.1.2
resolve: 1.22.8
@ -9256,6 +9376,8 @@ snapshots:
universalify: 0.2.0
url-parse: 1.5.10
tr46@0.0.3: {}
tr46@3.0.0:
dependencies:
punycode: 2.3.1
@ -9270,6 +9392,24 @@ snapshots:
ts-interface-checker@0.1.13: {}
ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 22.10.7
acorn: 8.14.0
acorn-walk: 8.3.2
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.7.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
tsconfig-paths@3.15.0:
dependencies:
'@types/json5': 0.0.29
@ -9391,6 +9531,8 @@ snapshots:
uuid@9.0.1: {}
v8-compile-cache-lib@3.0.1: {}
v8-to-istanbul@9.2.0:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
@ -9418,6 +9560,8 @@ snapshots:
dependencies:
makeerror: 1.0.12
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}
webpack-bundle-analyzer@4.10.1:
@ -9450,6 +9594,11 @@ snapshots:
tr46: 3.0.0
webidl-conversions: 7.0.0
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
@ -9543,6 +9692,8 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
yn@3.1.1: {}
yocto-queue@0.1.0: {}
zod-to-json-schema@3.24.1(zod@3.23.8):

View File

@ -0,0 +1,12 @@
import { Suspense } from 'react';
import GitHubForkStatusBadgeClient from './GitHubForkStatusBadgeClient';
import GitHubForkStatusBadgeServer from './GitHubForkStatusBadgeServer';
import { IS_DEVELOPMENT } from '@/site/config';
export default function GitHubForkStatusBadge() {
return IS_DEVELOPMENT
? <GitHubForkStatusBadgeClient label="Local" />
: <Suspense>
<GitHubForkStatusBadgeServer />
</Suspense>;
}

View File

@ -0,0 +1,75 @@
import Spinner from '@/components/Spinner';
import clsx from 'clsx/lite';
import Link from 'next/link';
import { BiLogoGithub } from 'react-icons/bi';
export default function GitHubForkStatusBadgeClient({
url,
label,
style = 'mono',
title,
}: {
url?: string
label?: string
style?: 'success' | 'warning' | 'mono'
title?: string
}) {
const classNameForStyle = () => {
switch (style) {
case 'success': return clsx(
'text-green-700 hover:text-green-700',
'dark:text-green-400 dark:hover:text-green-400',
'bg-green-100/75 dark:bg-green-900/50',
'border-green-300/25',
);
case 'warning': return clsx(
'text-amber-700 hover:text-amber-700',
'dark:text-amber-400 dark:hover:text-amber-400',
'bg-amber-100/75 dark:bg-amber-900/50',
'border-amber-300/25 dark:border-amber-900',
);
default: return clsx(
'text-gray-700 hover:text-gray-700',
'dark:text-gray-300 dark:hover:text-gray-300',
'bg-white dark:bg-transparent',
'border-main',
);
}
};
const className = clsx(
'opacity-0 transition-opacity animate-fade-in',
'inline-flex items-center gap-2',
'border transition-colors',
url ? 'hover:underline' : 'select-none',
'pl-[4.5px] pr-2.5 py-[3px]',
'rounded-full shadow-sm',
classNameForStyle(),
);
const content = <>
{!label
? <Spinner
color="text"
className="translate-x-[3px]"
/>
: <BiLogoGithub size={17} />}
{label ?? 'Checking'}
</>;
return url
? <Link
target="_blank"
href={url}
title={title}
className={className}
>
{content}
</Link>
: <span
title={title}
className={className}
>
{content}
</span>;
}

View File

@ -0,0 +1,31 @@
import GitHubForkStatusBadgeClient from './GitHubForkStatusBadgeClient';
import {
VERCEL_GIT_BRANCH,
VERCEL_GIT_REPO_OWNER,
VERCEL_GIT_REPO_SLUG,
} from '@/site/config';
import { getGitHubMetaWithFallback } from '.';
export default async function GitHubForkStatusBadgeServer() {
const owner = VERCEL_GIT_REPO_OWNER;
const repo = VERCEL_GIT_REPO_SLUG;
const branch = VERCEL_GIT_BRANCH;
const {
url,
isForkedFromBase,
isBaseRepo,
label,
title,
isBehind,
} = await getGitHubMetaWithFallback({ owner, repo, branch });
return isForkedFromBase || isBaseRepo
? <GitHubForkStatusBadgeClient {...{
url,
label,
title,
style: isBehind === undefined || isBehind ? 'warning' : 'mono',
}} />
: null;
}

153
src/admin/github/index.ts Normal file
View File

@ -0,0 +1,153 @@
import {
TEMPLATE_BASE_OWNER,
TEMPLATE_BASE_REPO,
TEMPLATE_BASE_BRANCH,
} from '@/site/config';
const DEFAULT_BRANCH = 'main';
const FALLBACK_TEXT = 'Unknown';
// Cache all results for 2 minutes to avoid rate limiting
// GitHub API requests limited to 60 requests per hour
const FETCH_CONFIG: RequestInit = {
next: { revalidate: 120 },
};
interface RepoParams {
owner?: string
repo?: string
branch?: string
};
// Website urls
const getGitHubRepoUrl = ({
owner = TEMPLATE_BASE_OWNER,
repo = TEMPLATE_BASE_REPO,
}: RepoParams = {}) =>
`https://github.com/${owner}/${repo}`;
export const getGitHubCompareUrl = ({
owner,
repo,
branch = DEFAULT_BRANCH,
}: RepoParams = {}) =>
// eslint-disable-next-line max-len
`${getGitHubRepoUrl({ owner, repo })}/compare/${branch}...${TEMPLATE_BASE_OWNER}:${TEMPLATE_BASE_REPO}:${TEMPLATE_BASE_BRANCH}`;
// API urls
const getGitHubApiRepoUrl = ({
owner = TEMPLATE_BASE_OWNER,
repo = TEMPLATE_BASE_REPO,
}: RepoParams = {}) =>
`https://api.github.com/repos/${owner}/${repo}`;
const getGitHubApiCommitsUrl = (params?: RepoParams) =>
`${getGitHubApiRepoUrl(params)}/commits/main`;
const getGitHubApiForksUrl = (params?: RepoParams) =>
`${getGitHubApiRepoUrl(params)}/forks`;
const getGitHubApiCompareUrl = ({
owner,
repo,
branch = 'main',
}: RepoParams = {}) =>
// eslint-disable-next-line max-len
`${getGitHubApiRepoUrl()}/compare/${TEMPLATE_BASE_BRANCH}...${owner}:${repo}:${branch}`;
// Requests
export const getLatestBaseRepoCommitSha = async () => {
const response = await fetch(getGitHubApiCommitsUrl(), FETCH_CONFIG);
const data = await response.json();
return data.sha ? data.sha.slice(0, 7) as string : undefined;
};
const getIsRepoForkedFromBase = async (params: RepoParams) => {
const response = await fetch(getGitHubApiRepoUrl(params), FETCH_CONFIG);
const data = await response.json();
return (
Boolean(data.fork) &&
data.source?.full_name === `${TEMPLATE_BASE_OWNER}/${TEMPLATE_BASE_REPO}`
);
};
const getGitHubCommitsBehind = async (params?: RepoParams) => {
const response = await fetch(getGitHubApiCompareUrl(params), FETCH_CONFIG);
const data = await response.json();
return data.behind_by as number;
};
const isRepoBaseRepo = async ({ owner, repo }: RepoParams) =>
owner?.toLowerCase() === TEMPLATE_BASE_OWNER &&
repo?.toLowerCase() === TEMPLATE_BASE_REPO;
export const getGitHubPublicFork = async (
params?: RepoParams,
): Promise<RepoParams> => {
const response = await fetch(getGitHubApiForksUrl(params), FETCH_CONFIG);
const fork = (await response.json())[0];
return {
owner: fork.owner.login,
repo: fork.name,
};
};
const getGitHubMeta = async (params: RepoParams) => {
const [
url,
isForkedFromBase,
isBaseRepo,
behindBy,
] = await Promise.all([
getGitHubRepoUrl(params),
getIsRepoForkedFromBase(params),
isRepoBaseRepo(params),
getGitHubCommitsBehind(params),
]);
const isBehind = behindBy === undefined
? undefined
: behindBy > 0;
const label = isBehind === undefined
? FALLBACK_TEXT
: isBehind
? `${behindBy} Behind`
: 'Synced';
const title = isBehind === undefined
? FALLBACK_TEXT
: isBehind
// eslint-disable-next-line max-len
? `This fork is ${behindBy} commit${behindBy === 1 ? '' : 's'} behind. Consider syncing on GitHub for the latest updates.`
: 'This fork is up to date.';
return {
url,
isForkedFromBase,
isBaseRepo,
behindBy,
isBehind,
label,
title,
};
};
export const getGitHubMetaWithFallback = (params: RepoParams) =>
getGitHubMeta(params)
.catch(e => {
console.error('Error retrieving GitHub meta', { params, error: e });
return {
url: undefined,
isForkedFromBase: false,
isBaseRepo: undefined,
behindBy: undefined,
isBehind: undefined,
label: FALLBACK_TEXT,
title: FALLBACK_TEXT,
};
});

View File

@ -1,6 +1,8 @@
import ClearCacheButton from '@/admin/ClearCacheButton';
import GitHubForkStatusBadge from '@/admin/github/GitHubForkStatusBadge';
import Container from '@/components/Container';
import SiteGrid from '@/components/SiteGrid';
import { IS_DEVELOPMENT, IS_VERCEL_GIT_PROVIDER_GITHUB } from '@/site/config';
import SiteChecklist from '@/site/SiteChecklist';
export default async function AdminConfigurationPage() {
@ -8,10 +10,12 @@ export default async function AdminConfigurationPage() {
<SiteGrid
contentMain={
<div className="space-y-4">
<div className="flex items-center">
<div className="flex-grow">
<div className="flex items-center gap-4">
<div className="grow">
App Configuration
</div>
{(IS_VERCEL_GIT_PROVIDER_GITHUB || IS_DEVELOPMENT) &&
<GitHubForkStatusBadge />}
<ClearCacheButton />
</div>
<Container spaceChildren={false}>

View File

@ -11,7 +11,7 @@ export default function SiteChecklist({
return (
<Suspense fallback={<SiteChecklistClient {...{
...CONFIG_CHECKLIST_STATUS,
isTestingConnections: true,
isAnalyzingConfiguration: true,
simplifiedView,
}} /> }>
<SiteChecklistServer {...{ simplifiedView }} />

View File

@ -98,11 +98,11 @@ export default function SiteChecklistClient({
aiError,
// Component props
simplifiedView,
isTestingConnections,
isAnalyzingConfiguration,
}: ConfigChecklistStatus &
Partial<Awaited<ReturnType<typeof testConnectionsAction>>> & {
simplifiedView?: boolean
isTestingConnections?: boolean
isAnalyzingConfiguration?: boolean
}) {
const renderLink = (href: string, text: string, external = true) =>
<>
@ -214,11 +214,11 @@ export default function SiteChecklistClient({
icon={<BiData size={16} />}
>
<ChecklistRow
title={hasDatabase && isTestingConnections
title={hasDatabase && isAnalyzingConfiguration
? 'Testing database connection'
: 'Setup database'}
status={hasDatabase}
isPending={hasDatabase && isTestingConnections}
isPending={hasDatabase && isAnalyzingConfiguration}
>
{databaseError && renderError({
connection: { provider: 'Database', error: databaseError},
@ -245,7 +245,7 @@ export default function SiteChecklistClient({
</ChecklistRow>
<ChecklistRow
title={
hasStorageProvider && isTestingConnections
hasStorageProvider && isAnalyzingConfiguration
? 'Testing storage connection'
: !hasStorageProvider
? 'Setup storage (one of the following)'
@ -254,7 +254,7 @@ export default function SiteChecklistClient({
? `Setup storage (new uploads go to: ${labelForStorage(currentStorage)})`
: 'Setup storage'}
status={hasStorageProvider}
isPending={hasStorageProvider && isTestingConnections}
isPending={hasStorageProvider && isAnalyzingConfiguration}
>
{storageError && renderError({
connection: { provider: 'Storage', error: storageError},
@ -300,11 +300,11 @@ export default function SiteChecklistClient({
icon={<BiLockAlt size={16} />}
>
<ChecklistRow
title={!hasAuthSecret && isTestingConnections
title={!hasAuthSecret && isAnalyzingConfiguration
? 'Generating secret'
: 'Setup auth'}
status={hasAuthSecret}
isPending={!hasAuthSecret && isTestingConnections}
isPending={!hasAuthSecret && isAnalyzingConfiguration}
>
Store auth secret in environment variable:
{!hasAuthSecret &&
@ -376,11 +376,11 @@ export default function SiteChecklistClient({
optional
>
<ChecklistRow
title={isAiTextGenerationEnabled && isTestingConnections
title={isAiTextGenerationEnabled && isAnalyzingConfiguration
? 'Testing OpenAI connection'
: 'Add OpenAI secret key'}
status={isAiTextGenerationEnabled}
isPending={isAiTextGenerationEnabled && isTestingConnections}
isPending={isAiTextGenerationEnabled && isAnalyzingConfiguration}
optional
>
{aiError && renderError({
@ -392,11 +392,11 @@ export default function SiteChecklistClient({
{renderEnvVars(['OPENAI_SECRET_KEY'])}
</ChecklistRow>
<ChecklistRow
title={hasVercelKv && isTestingConnections
title={hasVercelKv && isAnalyzingConfiguration
? 'Testing KV connection'
: 'Enable rate limiting'}
status={hasVercelKv}
isPending={hasVercelKv && isTestingConnections}
isPending={hasVercelKv && isAnalyzingConfiguration}
optional
>
{kvError && renderError({

View File

@ -8,6 +8,7 @@ export default async function SiteChecklistServer({
simplifiedView?: boolean
}) {
const connectionErrors = await testConnectionsAction().catch(() => ({}));
return (
<SiteChecklistClient {...{
...CONFIG_CHECKLIST_STATUS,

View File

@ -3,43 +3,46 @@ import type { StorageType } from '@/services/storage';
import { makeUrlAbsolute, shortenUrl } from '@/utility/url';
// HARD-CODED GLOBAL CONFIGURATION
export const SHOULD_PREFETCH_ALL_LINKS: boolean | undefined = undefined;
export const SHOULD_DEBUG_SQL = false;
// META / SOURCE / DOMAINS
export const SITE_TITLE =
process.env.NEXT_PUBLIC_SITE_TITLE ||
'Photo Blog';
// SOURCE
const VERCEL_GIT_PROVIDER =
export const TEMPLATE_BASE_OWNER = 'sambecker';
export const TEMPLATE_BASE_REPO = 'exif-photo-blog';
export const TEMPLATE_BASE_BRANCH = 'main';
export const VERCEL_GIT_PROVIDER =
process.env.NEXT_PUBLIC_VERCEL_GIT_PROVIDER;
const VERCEL_GIT_REPO_OWNER =
export const VERCEL_GIT_REPO_OWNER =
process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER;
const VERCEL_GIT_REPO_SLUG =
export const VERCEL_GIT_REPO_SLUG =
process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG;
const VERCEL_GIT_COMMIT_MESSAGE =
export const VERCEL_GIT_BRANCH = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF;
export const VERCEL_GIT_COMMIT_MESSAGE =
process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE;
const VERCEL_GIT_COMMIT_SHA =
export const VERCEL_GIT_COMMIT_SHA =
process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA;
const VERCEL_GIT_COMMIT_SHA_SHORT = VERCEL_GIT_COMMIT_SHA
export const VERCEL_GIT_COMMIT_SHA_SHORT = VERCEL_GIT_COMMIT_SHA
? VERCEL_GIT_COMMIT_SHA.slice(0, 7)
: undefined;
const VERCEL_GIT_COMMIT_URL = VERCEL_GIT_PROVIDER === 'github'
export const IS_VERCEL_GIT_PROVIDER_GITHUB = VERCEL_GIT_PROVIDER === 'github';
export const VERCEL_GIT_COMMIT_URL = IS_VERCEL_GIT_PROVIDER_GITHUB
// eslint-disable-next-line max-len
? `https://github.com/${VERCEL_GIT_REPO_OWNER}/${VERCEL_GIT_REPO_SLUG}/commit/${VERCEL_GIT_COMMIT_SHA}`
: undefined;
const VERCEL_ENV = process.env.NEXT_PUBLIC_VERCEL_ENV;
const VERCEL_PRODUCTION_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL;
const VERCEL_DEPLOYMENT_URL = process.env.NEXT_PUBLIC_VERCEL_URL;
const VERCEL_BRANCH_URL = process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL;
const VERCEL_BRANCH = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF;
export const VERCEL_ENV = process.env.NEXT_PUBLIC_VERCEL_ENV;
export const VERCEL_PRODUCTION_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL;
export const VERCEL_DEPLOYMENT_URL = process.env.NEXT_PUBLIC_VERCEL_URL;
export const VERCEL_BRANCH_URL = process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL;
// Last resort: cannot be used reliably
const VERCEL_PROJECT_URL = VERCEL_BRANCH_URL && VERCEL_BRANCH
? `${VERCEL_BRANCH_URL.split(`-git-${VERCEL_BRANCH}-`)[0]}.vercel.app`
export const VERCEL_PROJECT_URL = VERCEL_BRANCH_URL && VERCEL_GIT_BRANCH
? `${VERCEL_BRANCH_URL.split(`-git-${VERCEL_GIT_BRANCH}-`)[0]}.vercel.app`
: undefined;
export const IS_PRODUCTION = process.env.NODE_ENV === 'production' && (
@ -48,6 +51,7 @@ export const IS_PRODUCTION = process.env.NODE_ENV === 'production' && (
!VERCEL_ENV
);
export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
export const IS_PREVIEW = VERCEL_ENV === 'preview';
export const VERCEL_BYPASS_KEY = 'x-vercel-protection-bypass';

View File

@ -155,6 +155,9 @@
text-red-500 dark:text-red-400
}
/* Utilities: Border */
.border-main {
@apply border-gray-200 dark:border-gray-700
}
.border-subtle {
@apply
border border-gray-200 dark:border-gray-800

View File

@ -1,8 +0,0 @@
const GITHUB_API_URL =
'https://api.github.com/repos/sambecker/exif-photo-blog/commits/main';
export const fetchLatestRepoCommit = async () => {
const response = await fetch(GITHUB_API_URL);
const data = await response.json();
return data.sha.slice(0, 7);
};

View File

@ -29,7 +29,7 @@ module.exports = {
'rotate-pulse':
'rotate-pulse 0.75s linear infinite normal both running',
'fade-in':
'fade-in 0.5s linear',
'fade-in 0.5s linear both running',
'hover-drift':
'hover-drift 8s linear infinite',
'hover-wobble':