Compare commits

...

28 Commits

Author SHA1 Message Date
63481b4a4c feat: add Not Found component 2026-03-23 11:58:58 -06:00
1e03ca57b9 refactor: move Header and Footer to root layout 2026-03-23 11:58:04 -06:00
5c9e967163 feat: add Header component to FullWidth layout 2026-03-23 10:29:33 -06:00
4187c3bd80 feat: add Menu component with sub-components 2026-03-23 10:29:16 -06:00
b0617d89e8 feat: add Header and MainMenu components 2026-03-23 10:28:35 -06:00
e0c9d7c336 chore: update dependencies 2026-03-23 09:29:49 -06:00
a52327611b chore: update dependencies 2026-03-23 08:53:40 -06:00
a75239f4a4 feat: integrate @base-ui/react 2026-03-20 13:11:13 -06:00
f42c4b8e0a feat: add reusable Button component 2026-03-20 11:40:07 -06:00
9b8b8de875 feat: add srOnly class 2026-03-20 11:39:32 -06:00
2fc7de2cc8 chore: update dependencies 2026-03-20 10:29:53 -06:00
7155f55d73 fix: add Footer import to FullWidth component 2026-03-17 16:56:30 -06:00
5ffb305bb0 feat: add footer component to full-width layout 2026-03-17 16:44:54 -06:00
1771924c69 chore: upgrade Vite to v8 and update React plugins 2026-03-17 16:27:19 -06:00
cb7e4d3449 feat: add graceful shutdown handlers for SIGINT and SIGTERM 2026-03-09 14:16:28 -06:00
a6642bc2cc feat: add Docker configuration 2026-03-09 14:13:23 -06:00
19191eac7c chore: move vite-tsconfig-paths to dependencies
Moved vite-tsconfig-paths from devDependencies to dependencies as it is required at runtime for path resolution in the Vite build.
2026-03-09 12:19:27 -06:00
9589f3fa76 chore: update dependencies 2026-03-09 12:10:39 -06:00
a475d4c5a0 feat: changes deployment method from nitro to bun native server 2026-03-09 12:10:24 -06:00
de91f39231 feat(home): add logo and placeholder content to home page 2026-03-06 18:49:13 -06:00
03b988b5f5 feat(types): add MergeOmitting type helper for type merging with omission 2026-03-06 18:48:58 -06:00
59fc3bf686 feat: add SrJuggernaut logo component 2026-03-06 18:48:38 -06:00
e38871879c feat(layout): add full-height flex layout and FullWidth component 2026-03-06 18:48:13 -06:00
f4ad0bd9de feat: add favicon icons in PNG, ICO, and SVG formats 2026-03-06 18:47:17 -06:00
d5ec0d25ae feat: integrate Nitro nightly for enhanced server-side capabilities
- Add .output to .gitignore to exclude build output directory
2026-03-04 15:35:53 -06:00
20cf804ac6 feat: add Panda CSS styling system with fonts and icons
Add @pandacss/dev, srjuggernaut-panda-preset, FontAwesome icons, and fonts to enable styling.
Update .gitignore to exclude styled-system directories.
2026-03-04 14:30:44 -06:00
13819410c5 fix: exclude generated route tree file from Biome linting 2026-03-04 14:28:41 -06:00
b3970b04ab feat(vscode): add Biome formatter settings for consistent code style 2026-03-04 14:27:15 -06:00
32 changed files with 2339 additions and 196 deletions

40
.dockerignore Normal file
View File

@@ -0,0 +1,40 @@
node_modules
# output
out
dist
*.tgz
.nitro
.tanstack
.output
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
## Panda
styled-system
styled-system-studio

5
.gitignore vendored
View File

@@ -7,6 +7,7 @@ dist
*.tgz *.tgz
.nitro .nitro
.tanstack .tanstack
.output
# code coverage # code coverage
coverage coverage
@@ -34,3 +35,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config # Finder (MacOS) folder config
.DS_Store .DS_Store
## Panda
styled-system
styled-system-studio

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
}
}

51
Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
FROM oven/bun:1.3.10-alpine AS base
# Add metadata labels
LABEL maintainer="jugger@srjuggernaut.dev"
LABEL description="Blog application built with TanStack Start"
FROM base AS builder
WORKDIR /app
COPY . .
# Set proper environment
ENV BUILD=true
# Install all dependencies for build (dev + prod)
RUN bun install --frozen-lockfile
RUN bun run build
FROM base AS installer
WORKDIR /app
COPY . .
# Set proper environment
ENV NODE_ENV=production
# Install all dependencies for production
RUN bun install --frozen-lockfile --production
FROM base AS runner
WORKDIR /app
# Install curl for health checks
RUN apk add --no-cache curl
COPY --from=installer /app/node_modules /app/node_modules
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/server.ts /app/server.ts
ENV NODE_ENV=production
EXPOSE 3000/tcp
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/ || exit 1
CMD ["bun", "server.ts"]

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.4.5/schema.json", "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
"vcs": { "vcs": {
"enabled": true, "enabled": true,
"clientKind": "git", "clientKind": "git",
@@ -7,7 +7,7 @@
}, },
"files": { "files": {
"ignoreUnknown": true, "ignoreUnknown": true,
"includes": ["**", "!!node_modules", "!!bun.lock"] "includes": ["**", "!!node_modules", "!!bun.lock", "!!src/routeTree.gen.ts"]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,

814
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -6,27 +6,35 @@
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"prepare": "bun run ./.husky/install.mts" "prepare": "panda codegen && bun run ./.husky/install.mts"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.5", "@biomejs/biome": "2.4.8",
"@commitlint/cli": "^20.4.3", "@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.4.3", "@commitlint/config-conventional": "^20.5.0",
"@types/bun": "latest", "@pandacss/dev": "^1.9.1",
"@types/bun": "1.3.11",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.3.2", "lint-staged": "^16.4.0",
"vite": "^7.3.1", "vite": "^8.0.2"
"vite-tsconfig-paths": "^6.1.1"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-router": "^1.163.3", "@base-ui/react": "^1.3.0",
"@tanstack/react-start": "^1.166.1", "@fontsource-variable/roboto": "^5.2.10",
"@vitejs/plugin-react-swc": "^4.2.3", "@fontsource/orbitron": "^5.2.8",
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/react-fontawesome": "^3.3.0",
"@srjuggernaut-dev/srjuggernaut-panda-preset": "^0.0.17",
"@tanstack/react-router": "^1.168.2",
"@tanstack/react-start": "^1.167.5",
"@vitejs/plugin-react": "^6.0.1",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4" "react-dom": "^19.2.4"
} }

69
panda.config.ts Normal file
View File

@@ -0,0 +1,69 @@
import { defineConfig } from '@pandacss/dev'
import srJuggernautPandaPreset from '@srjuggernaut-dev/srjuggernaut-panda-preset'
import themeConfig from '@/styles/theme'
export default defineConfig({
presets: [srJuggernautPandaPreset(themeConfig)],
// Whether to use css reset
preflight: true,
// Where to look for your css declarations
include: ['./src/**/*.{js,jsx,ts,tsx}'],
// Files to exclude
exclude: [],
// Useful for theme customization
theme: {
extend: {}
},
globalCss: {
body: {
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
backgroundColor: 'neutral.1',
color: 'neutral.12',
fontFamily: "'Roboto Variable', sans-serif",
lineHeight: 'tight'
},
'*, *::before, *::after': {
margin: 0,
padding: 0,
boxSizing: 'border-box'
},
'h1, h2, h3, h4, h5, h6': {
fontWeight: '700',
textWrap: 'balance',
lineHeight: 'normal'
},
h1: {
fontFamily: 'orbitron',
fontWeight: '900',
fontSize: 'h1'
},
h2: {
fontSize: 'h2'
},
h3: {
fontSize: 'h3'
},
h4: {
fontSize: 'h4'
},
h5: {
fontSize: 'h5'
},
h6: {
fontSize: 'h6'
},
p: {
fontSize: 'body',
textWrap: 'pretty'
}
},
// The output directory for your css system
outdir: 'styled-system'
})

6
postcss.config.ts Normal file
View File

@@ -0,0 +1,6 @@
import pandacssPlugin from '@pandacss/dev/postcss'
import type { Plugin, Processor, Transformer } from 'postcss'
export default {
plugins: [pandacssPlugin()]
} as { plugins: (Plugin | Transformer | Processor)[] }

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

146
public/favicon.svg Normal file
View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18-5">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-900.36955,-982.1127)"
id="path18-6" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath16-2">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-1121.3406,-984.5247)"
id="path16-9" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath14-1">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-1166.4306,-571.09469)"
id="path14-2" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath12-7">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-833.56555,-568.67474)"
id="path12-0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath10-9">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-1003.9206,-978.73275)"
id="path10-3" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath8-6">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-606.82952,-1591.4017)"
id="path8-0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath6-6">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-1400.4006,-1591.4017)"
id="path6-2" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath4-6">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-862.51052,-672.39472)"
id="path4-1" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2-8">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-1144.7205,-672.39472)"
id="path2-7" />
</clipPath>
</defs>
<g
id="g1"
transform="matrix(0.21333333,0,0,0.21300855,276.48,196.25889)"
style="fill:#eeeeee;fill-opacity:1;paint-order:markers fill stroke;stroke:#111111;stroke-opacity:1;stroke-width:9.38214458;stroke-dasharray:none;stroke-linejoin:round;stroke-linecap:round">
<path
id="path173"
d="M 0,0 V 281.011 L 145.56,378.7 193.8,496.063 -38.59,395.585 48.25,712.774 274.98,903.33 c 0,0 71.16,-55.478 137.49,-172.464 V 436.59 Z"
style="display:inline;fill-opacity:1;fill-rule:evenodd;paint-order:markers fill stroke"
transform="matrix(1.1293021,0,0,-1.2103512,67.4332,899.72155)"
clip-path="url(#clipPath2-8)" />
<path
id="path174"
d="M 0,0 V 281.011 L -145.554,378.7 -193.796,496.063 38.593,395.585 -48.242,712.774 -274.978,903.33 c 0,0 -71.156,-55.48 -137.488,-172.466 V 436.59 Z"
style="display:inline;fill-opacity:1;fill-rule:evenodd;paint-order:markers fill stroke"
transform="matrix(1.1293021,0,0,-1.2103512,-251.2672,899.72155)"
clip-path="url(#clipPath4-6)" />
<path
id="path175"
d="m 0,0 -194.55,-164.022 66.71,253.269 c 0,0 91.66,-50.654 127.84,-89.247"
style="display:inline;fill-opacity:1;fill-rule:evenodd;paint-order:markers fill stroke"
transform="matrix(1.1293021,0,0,-1.2103512,356.1732,-212.59967)"
clip-path="url(#clipPath6-6)" />
<path
id="path176"
d="M 0,0 194.55,-164.022 127.841,89.247 C 127.841,89.247 36.181,38.593 0,0"
style="display:inline;fill-opacity:1;fill-rule:evenodd;paint-order:markers fill stroke"
transform="matrix(1.1293021,0,0,-1.2103512,-540.00824,-212.59967)"
clip-path="url(#clipPath8-6)" />
<path
id="path177"
d="m 0,0 68.44,50.654 183.32,677.795 c 0,0 -231.56,229.147 -501.714,0 33.769,-118.192 183.318,-677.8 183.318,-677.8 z"
style="display:inline;fill-opacity:1;fill-rule:evenodd;paint-order:markers fill stroke"
transform="matrix(1.1293021,0,0,-1.2103512,-91.5725,528.94505)"
clip-path="url(#clipPath10-9)" />
<path
id="path178"
d="m 0,0 -26.533,74.78 -410.054,446.234 -2.412,168.845 -156.785,-118.192 -125.429,-69.95 -108.543,-106.132 173.669,28.945 176.083,-94.071 -14.473,-50.654 L -62.714,-9.64 Z"
style="display:inline;fill-opacity:1;fill-rule:evenodd;paint-order:markers fill stroke"
transform="matrix(1.1293021,0,0,-1.2103512,-283.95482,1025.2591)"
clip-path="url(#clipPath12-7)" />
<path
id="path179"
d="M 0,0 26.54,74.77 436.59,521.006 439,689.851 595.79,571.659 721.21,501.709 829.76,395.578 656.09,424.522 480.01,330.451 494.48,279.798 62.72,-9.65 Z"
style="display:inline;fill-opacity:1;fill-rule:evenodd;paint-order:markers fill stroke"
transform="matrix(1.1293021,0,0,-1.2103512,91.9503,1022.3302)"
clip-path="url(#clipPath14-1)" />
<path
id="path180"
d="M 0,0 28.58,66.523 170.31,141.547 148.07,83.058 Z"
style="display:inline;fill-opacity:1;fill-rule:evenodd;paint-order:markers fill stroke"
transform="matrix(1.1293021,0,0,-1.2103512,41.03,521.93465)"
clip-path="url(#clipPath16-2)" />
<path
id="path181"
d="m 0,0 -28.579,66.523 -141.731,75.024 22.241,-58.489 z"
style="display:inline;fill-opacity:1;fill-rule:evenodd;paint-order:markers fill stroke"
transform="matrix(1.1293021,0,0,-1.2103512,-208.5129,524.85405)"
clip-path="url(#clipPath18-5)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

566
server.ts Normal file
View File

@@ -0,0 +1,566 @@
/**
* TanStack Start Production Server with Bun
*
* A high-performance production server for TanStack Start applications that
* implements intelligent static asset loading with configurable memory management.
*
* Features:
* - Hybrid loading strategy (preload small files, serve large files on-demand)
* - Configurable file filtering with include/exclude patterns
* - Memory-efficient response generation
* - Production-ready caching headers
*
* Environment Variables:
*
* PORT (number)
* - Server port number
* - Default: 3000
*
* ASSET_PRELOAD_MAX_SIZE (number)
* - Maximum file size in bytes to preload into memory
* - Files larger than this will be served on-demand from disk
* - Default: 5242880 (5MB)
* - Example: ASSET_PRELOAD_MAX_SIZE=5242880 (5MB)
*
* ASSET_PRELOAD_INCLUDE_PATTERNS (string)
* - Comma-separated list of glob patterns for files to include
* - If specified, only matching files are eligible for preloading
* - Patterns are matched against filenames only, not full paths
* - Example: ASSET_PRELOAD_INCLUDE_PATTERNS="*.js,*.css,*.woff2"
*
* ASSET_PRELOAD_EXCLUDE_PATTERNS (string)
* - Comma-separated list of glob patterns for files to exclude
* - Applied after include patterns
* - Patterns are matched against filenames only, not full paths
* - Example: ASSET_PRELOAD_EXCLUDE_PATTERNS="*.map,*.txt"
*
* ASSET_PRELOAD_VERBOSE_LOGGING (boolean)
* - Enable detailed logging of loaded and skipped files
* - Default: false
* - Set to "true" to enable verbose output
*
* ASSET_PRELOAD_ENABLE_ETAG (boolean)
* - Enable ETag generation for preloaded assets
* - Default: true
* - Set to "false" to disable ETag support
*
* ASSET_PRELOAD_ENABLE_GZIP (boolean)
* - Enable Gzip compression for eligible assets
* - Default: true
* - Set to "false" to disable Gzip compression
*
* ASSET_PRELOAD_GZIP_MIN_SIZE (number)
* - Minimum file size in bytes required for Gzip compression
* - Files smaller than this will not be compressed
* - Default: 1024 (1KB)
*
* ASSET_PRELOAD_GZIP_MIME_TYPES (string)
* - Comma-separated list of MIME types eligible for Gzip compression
* - Supports partial matching for types ending with "/"
* - Default: text/,application/javascript,application/json,application/xml,image/svg+xml
*
* Usage:
* bun run server.ts
*/
import path from 'node:path'
// Configuration
const SERVER_PORT = Number(process.env.PORT ?? 3000)
const CLIENT_DIRECTORY = './dist/client'
const SERVER_ENTRY_POINT = './dist/server/server.js'
// Logging utilities for professional output
const log = {
info: (message: string) => {
console.log(`[INFO] ${message}`)
},
success: (message: string) => {
console.log(`[SUCCESS] ${message}`)
},
warning: (message: string) => {
console.log(`[WARNING] ${message}`)
},
error: (message: string) => {
console.log(`[ERROR] ${message}`)
},
header: (message: string) => {
console.log(`\n${message}\n`)
}
}
// Preloading configuration from environment variables
const MAX_PRELOAD_BYTES = Number(
process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024 // 5MB default
)
// Parse comma-separated include patterns (no defaults)
const INCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((pattern: string) => convertGlobToRegExp(pattern))
// Parse comma-separated exclude patterns (no defaults)
const EXCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((pattern: string) => convertGlobToRegExp(pattern))
// Verbose logging flag
const VERBOSE = process.env.ASSET_PRELOAD_VERBOSE_LOGGING === 'true'
// Optional ETag feature
const ENABLE_ETAG = (process.env.ASSET_PRELOAD_ENABLE_ETAG ?? 'true') === 'true'
// Optional Gzip feature
const ENABLE_GZIP = (process.env.ASSET_PRELOAD_ENABLE_GZIP ?? 'true') === 'true'
const GZIP_MIN_BYTES = Number(process.env.ASSET_PRELOAD_GZIP_MIN_SIZE ?? 1024) // 1KB
const GZIP_TYPES = (
process.env.ASSET_PRELOAD_GZIP_MIME_TYPES ??
'text/,application/javascript,application/json,application/xml,image/svg+xml'
)
.split(',')
.map((v) => v.trim())
.filter(Boolean)
/**
* Convert a simple glob pattern to a regular expression
* Supports * wildcard for matching any characters
*/
function convertGlobToRegExp(globPattern: string): RegExp {
// Escape regex special chars except *, then replace * with .*
const escapedPattern = globPattern
.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&')
.replace(/\*/g, '.*')
return new RegExp(`^${escapedPattern}$`, 'i')
}
/**
* Compute ETag for a given data buffer
*/
function computeEtag(data: Uint8Array): string {
const hash = Bun.hash(data)
return `W/"${hash.toString(16)}-${data.byteLength.toString()}"`
}
/**
* Metadata for preloaded static assets
*/
interface AssetMetadata {
route: string
size: number
type: string
}
/**
* In-memory asset with ETag and Gzip support
*/
interface InMemoryAsset {
raw: Uint8Array
gz?: Uint8Array
etag?: string
type: string
immutable: boolean
size: number
}
/**
* Result of static asset preloading process
*/
interface PreloadResult {
routes: Record<string, (req: Request) => Response | Promise<Response>>
loaded: AssetMetadata[]
skipped: AssetMetadata[]
}
/**
* Check if a file is eligible for preloading based on configured patterns
*/
function isFileEligibleForPreloading(relativePath: string): boolean {
const fileName = relativePath.split(/[/\\]/).pop() ?? relativePath
// If include patterns are specified, file must match at least one
if (INCLUDE_PATTERNS.length > 0) {
if (!INCLUDE_PATTERNS.some((pattern) => pattern.test(fileName))) {
return false
}
}
// If exclude patterns are specified, file must not match any
if (EXCLUDE_PATTERNS.some((pattern) => pattern.test(fileName))) {
return false
}
return true
}
/**
* Check if a MIME type is compressible
*/
function isMimeTypeCompressible(mimeType: string): boolean {
return GZIP_TYPES.some((type) =>
type.endsWith('/') ? mimeType.startsWith(type) : mimeType === type
)
}
/**
* Conditionally compress data based on size and MIME type
*/
function compressDataIfAppropriate(
data: Uint8Array,
mimeType: string
): Uint8Array | undefined {
if (!ENABLE_GZIP) return undefined
if (data.byteLength < GZIP_MIN_BYTES) return undefined
if (!isMimeTypeCompressible(mimeType)) return undefined
try {
return Bun.gzipSync(data.buffer as ArrayBuffer)
} catch {
return undefined
}
}
/**
* Create response handler function with ETag and Gzip support
*/
function createResponseHandler(
asset: InMemoryAsset
): (req: Request) => Response {
return (req: Request) => {
const headers: Record<string, string> = {
'Content-Type': asset.type,
'Cache-Control': asset.immutable
? 'public, max-age=31536000, immutable'
: 'public, max-age=3600'
}
if (ENABLE_ETAG && asset.etag) {
const ifNone = req.headers.get('if-none-match')
if (ifNone && ifNone === asset.etag) {
return new Response(null, {
status: 304,
headers: { ETag: asset.etag }
})
}
headers.ETag = asset.etag
}
if (
ENABLE_GZIP &&
asset.gz &&
req.headers.get('accept-encoding')?.includes('gzip')
) {
headers['Content-Encoding'] = 'gzip'
headers['Content-Length'] = String(asset.gz.byteLength)
const gzCopy = new Uint8Array(asset.gz)
return new Response(gzCopy, { status: 200, headers })
}
headers['Content-Length'] = String(asset.raw.byteLength)
const rawCopy = new Uint8Array(asset.raw)
return new Response(rawCopy, { status: 200, headers })
}
}
/**
* Create composite glob pattern from include patterns
*/
function createCompositeGlobPattern(): Bun.Glob {
const raw = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
if (raw.length === 0) return new Bun.Glob('**/*')
if (raw.length === 1) return new Bun.Glob(raw[0] as string)
return new Bun.Glob(`{${raw.join(',')}}`)
}
/**
* Initialize static routes with intelligent preloading strategy
* Small files are loaded into memory, large files are served on-demand
*/
async function initializeStaticRoutes(
clientDirectory: string
): Promise<PreloadResult> {
const routes: Record<string, (req: Request) => Response | Promise<Response>> =
{}
const loaded: AssetMetadata[] = []
const skipped: AssetMetadata[] = []
log.info(`Loading static assets from ${clientDirectory}...`)
if (VERBOSE) {
console.log(
`Max preload size: ${(MAX_PRELOAD_BYTES / 1024 / 1024).toFixed(2)} MB`
)
if (INCLUDE_PATTERNS.length > 0) {
console.log(
`Include patterns: ${process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? ''}`
)
}
if (EXCLUDE_PATTERNS.length > 0) {
console.log(
`Exclude patterns: ${process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? ''}`
)
}
}
let totalPreloadedBytes = 0
try {
const glob = createCompositeGlobPattern()
for await (const relativePath of glob.scan({ cwd: clientDirectory })) {
const filepath = path.join(clientDirectory, relativePath)
const route = `/${relativePath.split(path.sep).join(path.posix.sep)}`
try {
// Get file metadata
const file = Bun.file(filepath)
// Skip if file doesn't exist or is empty
if (!(await file.exists()) || file.size === 0) {
continue
}
const metadata: AssetMetadata = {
route,
size: file.size,
type: file.type || 'application/octet-stream'
}
// Determine if file should be preloaded
const matchesPattern = isFileEligibleForPreloading(relativePath)
const withinSizeLimit = file.size <= MAX_PRELOAD_BYTES
if (matchesPattern && withinSizeLimit) {
// Preload small files into memory with ETag and Gzip support
const bytes = new Uint8Array(await file.arrayBuffer())
const gz = compressDataIfAppropriate(bytes, metadata.type)
const etag = ENABLE_ETAG ? computeEtag(bytes) : undefined
const asset: InMemoryAsset = {
raw: bytes,
gz,
etag,
type: metadata.type,
immutable: true,
size: bytes.byteLength
}
routes[route] = createResponseHandler(asset)
loaded.push({ ...metadata, size: bytes.byteLength })
totalPreloadedBytes += bytes.byteLength
} else {
// Serve large or filtered files on-demand
routes[route] = () => {
const fileOnDemand = Bun.file(filepath)
return new Response(fileOnDemand, {
headers: {
'Content-Type': metadata.type,
'Cache-Control': 'public, max-age=3600'
}
})
}
skipped.push(metadata)
}
} catch (error: unknown) {
if (error instanceof Error && error.name !== 'EISDIR') {
log.error(`Failed to load ${filepath}: ${error.message}`)
}
}
}
// Show detailed file overview only when verbose mode is enabled
if (VERBOSE && (loaded.length > 0 || skipped.length > 0)) {
const allFiles = [...loaded, ...skipped].sort((a, b) =>
a.route.localeCompare(b.route)
)
// Calculate max path length for alignment
const maxPathLength = Math.min(
Math.max(...allFiles.map((f) => f.route.length)),
60
)
// Format file size with KB and actual gzip size
const formatFileSize = (bytes: number, gzBytes?: number) => {
const kb = bytes / 1024
const sizeStr = kb < 100 ? kb.toFixed(2) : kb.toFixed(1)
if (gzBytes !== undefined) {
const gzKb = gzBytes / 1024
const gzStr = gzKb < 100 ? gzKb.toFixed(2) : gzKb.toFixed(1)
return {
size: sizeStr,
gzip: gzStr
}
}
// Rough gzip estimation (typically 30-70% compression) if no actual gzip data
const gzipKb = kb * 0.35
return {
size: sizeStr,
gzip: gzipKb < 100 ? gzipKb.toFixed(2) : gzipKb.toFixed(1)
}
}
if (loaded.length > 0) {
console.log('\n📁 Preloaded into memory:')
console.log(
'Path │ Size │ Gzip Size'
)
loaded
.sort((a, b) => a.route.localeCompare(b.route))
.forEach((file) => {
const { size, gzip } = formatFileSize(file.size)
const paddedPath = file.route.padEnd(maxPathLength)
const sizeStr = `${size.padStart(7)} kB`
const gzipStr = `${gzip.padStart(7)} kB`
console.log(`${paddedPath}${sizeStr}${gzipStr}`)
})
}
if (skipped.length > 0) {
console.log('\n💾 Served on-demand:')
console.log(
'Path │ Size │ Gzip Size'
)
skipped
.sort((a, b) => a.route.localeCompare(b.route))
.forEach((file) => {
const { size, gzip } = formatFileSize(file.size)
const paddedPath = file.route.padEnd(maxPathLength)
const sizeStr = `${size.padStart(7)} kB`
const gzipStr = `${gzip.padStart(7)} kB`
console.log(`${paddedPath}${sizeStr}${gzipStr}`)
})
}
}
// Show detailed verbose info if enabled
if (VERBOSE) {
if (loaded.length > 0 || skipped.length > 0) {
const allFiles = [...loaded, ...skipped].sort((a, b) =>
a.route.localeCompare(b.route)
)
console.log('\n📊 Detailed file information:')
console.log(
'Status │ Path │ MIME Type │ Reason'
)
allFiles.forEach((file) => {
const isPreloaded = loaded.includes(file)
const status = isPreloaded ? 'MEMORY' : 'ON-DEMAND'
const reason =
!isPreloaded && file.size > MAX_PRELOAD_BYTES
? 'too large'
: !isPreloaded
? 'filtered'
: 'preloaded'
const route =
file.route.length > 30
? file.route.substring(0, 27) + '...'
: file.route
console.log(
`${status.padEnd(12)}${route.padEnd(30)}${file.type.padEnd(28)}${reason.padEnd(10)}`
)
})
} else {
console.log('\n📊 No files found to display')
}
}
// Log summary after the file list
console.log() // Empty line for separation
if (loaded.length > 0) {
log.success(
`Preloaded ${String(loaded.length)} files (${(totalPreloadedBytes / 1024 / 1024).toFixed(2)} MB) into memory`
)
} else {
log.info('No files preloaded into memory')
}
if (skipped.length > 0) {
const tooLarge = skipped.filter((f) => f.size > MAX_PRELOAD_BYTES).length
const filtered = skipped.length - tooLarge
log.info(
`${String(skipped.length)} files will be served on-demand (${String(tooLarge)} too large, ${String(filtered)} filtered)`
)
}
} catch (error) {
log.error(
`Failed to load static files from ${clientDirectory}: ${String(error)}`
)
}
return { routes, loaded, skipped }
}
/**
* Initialize the server
*/
async function initializeServer() {
log.header('Starting Production Server')
// Load TanStack Start server handler
let handler: { fetch: (request: Request) => Response | Promise<Response> }
try {
const serverModule = (await import(SERVER_ENTRY_POINT)) as {
default: { fetch: (request: Request) => Response | Promise<Response> }
}
handler = serverModule.default
log.success('TanStack Start application handler initialized')
} catch (error) {
log.error(`Failed to load server handler: ${String(error)}`)
process.exit(1)
}
// Build static routes with intelligent preloading
const { routes } = await initializeStaticRoutes(CLIENT_DIRECTORY)
// Create Bun server
const server = Bun.serve({
port: SERVER_PORT,
routes: {
// Serve static assets (preloaded or on-demand)
...routes,
// Fallback to TanStack Start handler for all other routes
'/*': (req: Request) => {
try {
return handler.fetch(req)
} catch (error) {
log.error(`Server handler error: ${String(error)}`)
return new Response('Internal Server Error', { status: 500 })
}
}
},
// Global error handler
error(error) {
log.error(
`Uncaught server error: ${error instanceof Error ? error.message : String(error)}`
)
return new Response('Internal Server Error', { status: 500 })
}
})
process.on('SIGINT', () => {
log.info('Shutting down server...')
server.stop()
})
process.on('SIGTERM', () => {
log.info('Shutting down server...')
server.stop()
})
log.success(`Server listening on http://localhost:${String(server.port)}`)
}
// Initialize the server
initializeServer().catch((error: unknown) => {
log.error(`Failed to start server: ${String(error)}`)
process.exit(1)
})

View File

@@ -0,0 +1,35 @@
import { css } from '@styled-system/css'
import type { FC } from 'react'
import FullWidth from '@/components/layout/FullWidth'
const NotFoundStyle = css({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
})
const NotFound: FC = () => {
return (
<FullWidth className={NotFoundStyle}>
<span
className={css({
fontSize: '100px',
fontFamily: 'orbitron',
fontWeight: '900',
lineHeight: 'none',
color: 'primary.9'
})}
>
404
</span>
<h1 className={css({ fontSize: '45px' })}>Pagina no encontrada.</h1>
<p>
La pagina que estas buscando no existe o ha sido movida. Puedes volver a
la <a href="/">pagina principal</a>, o intentar de nuevo mas tarde.
</p>
</FullWidth>
)
}
export default NotFound

View File

@@ -0,0 +1,60 @@
import { css, cx } from '@styled-system/css'
import type { FC, SVGProps } from 'react'
export type SrJuggernautLogoProps = SVGProps<SVGSVGElement>
const SrJuggernautLogo: FC<SrJuggernautLogoProps> = ({
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2250 1513"
className={cx(
css({ display: 'inline-block', fill: 'currentColor', height: 'auto' }),
className
)}
{...props}
>
<title>Sr Juggernaut Logo</title>
<g>
<path
d="M0 0v281.011L145.56 378.7l48.24 117.363-232.39-100.478 86.84 317.189L274.98 903.33s71.16-55.478 137.49-172.464V436.59Z"
transform="matrix(1.1293 0 0 -1.21035 1288.433 1375.722)"
/>
<path
d="M0 0v281.011L-145.554 378.7l-48.242 117.363L38.593 395.585l-86.835 317.189-226.736 190.556s-71.156-55.48-137.488-172.466V436.59Z"
transform="matrix(1.1293 0 0 -1.21035 969.733 1375.722)"
/>
<path
d="m0 0-194.55-164.022 66.71 253.269S-36.18 38.593 0 0"
transform="matrix(1.1293 0 0 -1.21035 1577.173 263.4)"
/>
<path
d="m0 0 194.55-164.022-66.709 253.269S36.181 38.593 0 0"
transform="matrix(1.1293 0 0 -1.21035 680.992 263.4)"
/>
<path
d="m0 0 68.44 50.654 183.32 677.795s-231.56 229.147-501.714 0c33.769-118.192 183.318-677.8 183.318-677.8z"
transform="matrix(1.1293 0 0 -1.21035 1129.428 1004.945)"
/>
<path
d="m0 0-26.533 74.78-410.054 446.234-2.412 168.845-156.785-118.192-125.429-69.95-108.543-106.132 173.669 28.945 176.083-94.071-14.473-50.654L-62.714-9.64Z"
transform="matrix(1.1293 0 0 -1.21035 937.045 1501.26)"
/>
<path
d="m0 0 26.54 74.77 410.05 446.236L439 689.851l156.79-118.192 125.42-69.95 108.55-106.131-173.67 28.944-176.08-94.071 14.47-50.653L62.72-9.65Z"
transform="matrix(1.1293 0 0 -1.21035 1312.95 1498.33)"
/>
<path
d="m0 0 28.58 66.523 141.73 75.024-22.24-58.489Z"
transform="matrix(1.1293 0 0 -1.21035 1262.03 997.935)"
/>
<path
d="m0 0-28.579 66.523-141.731 75.024 22.241-58.489z"
transform="matrix(1.1293 0 0 -1.21035 1012.487 1000.854)"
/>
</g>
</svg>
)
export default SrJuggernautLogo

View File

@@ -0,0 +1,13 @@
import { css, cx } from '@styled-system/css'
import type { FC, ReactNode } from 'react'
export interface FullWidthProps {
className?: string
children?: ReactNode
}
const FullWidth: FC<FullWidthProps> = ({ className, children }) => {
return <main className={cx(css({ flexGrow: 1 }), className)}>{children}</main>
}
export default FullWidth

View File

@@ -0,0 +1,124 @@
import { faChevronRight, faHeart } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { css } from '@styled-system/css'
import type { FC } from 'react'
const footerClass = css({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
flexShrink: 0
})
const footerContainerClass = css({
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
width: '100%',
maxWidth: {
sm: 'breakpoint-sm',
md: 'breakpoint-md',
lg: 'breakpoint-lg',
xl: 'breakpoint-xl',
'2xl': 'breakpoint-2xl'
}
})
const footerLegendClass = css({
fontSize: 'sm'
})
const footerLegendIconClass = css({
color: 'red'
})
const footerLinkGroupClass = css({
padding: 'sm'
})
const footerLinkGroupTitleClass = css({
display: 'block',
fontSize: 'h3',
textAlign: 'center'
})
interface Link {
label: string
href: string
}
interface LinkGroup {
heading: string
links: Link[]
}
const linkGroups: LinkGroup[] = [
{
heading: 'Sobre Mi',
links: [{ label: 'Como Desarrollador', href: 'https://srjuggernaut.dev/' }]
},
{
heading: 'Redes Sociales',
links: [
{ label: 'Twitch', href: 'https://www.twitch.tv/juggernautplays' },
{ label: 'Youtube', href: 'https://www.youtube.com/JuggernautPlays' },
{
label: 'BlueSky',
href: 'https://bsky.app/profile/jugger.srjuggernaut.dev'
},
{ label: 'Twitter', href: 'https://twitter.com/juggernautplays' }
]
}
]
const Footer: FC = () => {
return (
<footer className={footerClass}>
<div className={footerContainerClass}>
{linkGroups.map((group) => (
<div
className={footerLinkGroupClass}
key={group.heading}
>
<span className={footerLinkGroupTitleClass}>{group.heading}</span>
<ul className="fa-ul">
{group.links.map((link) => (
<li key={link.href}>
<span className="fa-li">
<FontAwesomeIcon icon={faChevronRight} />
</span>
<a href={link.href}>{link.label}</a>
</li>
))}
</ul>
</div>
))}
</div>
<p className={footerLegendClass}>
Made by{' '}
<a
href="https://srjuggernaut.dev/"
target="_blank"
rel="noopener noreferrer"
>
SrJuggernaut
</a>{' '}
with{' '}
<FontAwesomeIcon
icon={faHeart}
className={footerLegendIconClass}
/>{' '}
and{' '}
<a
href="https://tanstack.com/start/"
target="_blank"
rel="noopener noreferrer"
>
TanStack Start
</a>
</p>
</footer>
)
}
export default Footer

View File

@@ -0,0 +1,96 @@
import { css, cx } from '@styled-system/css'
import { token } from '@styled-system/tokens'
import { type FC, useCallback, useEffect, useState } from 'react'
import SrJuggernautLogo from '@/components/assets/SrJuggernautLogo'
import MainMenu from '@/components/layout/fragments/MainMenu'
import srOnlyClass from '@/styles/srOnly'
const headerClass = css({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
position: 'sticky',
top: 0,
transition: `background-color ${token('durations.slow')} ${token('easings.easeOutQuint')}`,
zIndex: 1,
flexShrink: 0
})
const headerUnscrolledClass = css({
backgroundColor: 'transparent'
})
const headerScrolledClass = css({
backgroundColor: 'neutral.2'
})
const headerContainerClass = css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
padding: 'md',
maxWidth: {
sm: 'breakpoint-sm',
md: 'breakpoint-md',
lg: 'breakpoint-lg',
xl: 'breakpoint-xl',
'2xl': 'breakpoint-2xl'
}
})
const headerLogoLinkClass = css({
color: 'neutral.12',
cursor: 'pointer'
})
const headerLogoClass = css({
fill: 'neutral.12',
height: '30px',
width: 'auto'
})
const Header: FC = () => {
const [scrolled, setScrolled] = useState(false)
const handleScroll = useCallback(() => {
if (window.scrollY > 0) {
setScrolled(true)
} else {
setScrolled(false)
}
}, [])
useEffect(() => {
if (typeof window === 'undefined') {
return
}
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [handleScroll])
return (
<header
className={cx(
headerClass,
scrolled ? headerScrolledClass : headerUnscrolledClass
)}
>
<div className={headerContainerClass}>
<a
className={headerLogoLinkClass}
href="/"
>
<SrJuggernautLogo className={headerLogoClass} />
<span className={srOnlyClass}>Ir a la página principal</span>
</a>
<MainMenu />
</div>
</header>
)
}
export default Header

View File

@@ -0,0 +1,186 @@
import { faBars, faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { css } from '@styled-system/css'
import { token } from '@styled-system/tokens'
import { type FC, type ReactNode, useRef } from 'react'
import SrJuggernautLogo from '@/components/assets/SrJuggernautLogo'
import Button from '@/components/ui/Button'
import Menu, {
MenuGroup,
MenuItem,
MenuLabel,
MenuSeparator
} from '@/components/ui/Menu'
import srOnlyClass from '@/styles/srOnly'
const menuDialogClass = css({
position: 'fixed',
height: '100dvh',
width: {
base: '100%',
sm: '250px'
},
top: 0,
right: 0,
left: 'auto',
backgroundColor: 'neutral.2',
color: 'neutral.12',
transition: `transform ${token('durations.normal')} ${token('easings.easeOutQuint')}`,
transitionBehavior: 'allow-discrete',
transform: 'translateX(0)',
'@starting-style': {
transform: 'translateX(100%)',
_backdrop: {
opacity: 0,
backgroundColor: 'transparent',
transition: `background-color ${token('durations.normal')} ${token('easings.easeOutQuint')}, opacity ${token('durations.fast')} ${token('easings.easeOutQuint')}`,
transitionBehavior: 'allow-discrete'
}
},
_backdrop: {
opacity: 1,
backdropFilter: 'blur(5px)',
backgroundColor: 'neutral.1/80',
transition: `background-color ${token('durations.normal')} ${token('easings.easeOutQuint')}, opacity ${token('durations.fast')} ${token('easings.easeOutQuint')}`,
transitionBehavior: 'allow-discrete'
}
})
const menuHeaderContainerClass = css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
padding: 'md'
})
const menuHeaderLogoLinkClass = css({
color: 'neutral.12',
cursor: 'pointer'
})
const menuHeaderLogoClass = css({
fill: 'neutral.12',
height: '30px',
width: 'auto'
})
interface MainMenuLink {
type: 'link'
href: string
label: ReactNode
}
interface MainMenuLabel {
type: 'label'
label: ReactNode
}
interface MainMenuSeparator {
type: 'separator'
}
interface MainMenuGroup {
type: 'group'
label: ReactNode
content: (MainMenuLink | MainMenuLabel | MainMenuSeparator)[]
}
type MenuItemType =
| MainMenuLink
| MainMenuSeparator
| MainMenuLabel
| MainMenuGroup
const menuContent: MenuItemType[] = [
{ type: 'link', href: '/', label: 'Inicio' }
]
const RenderMenuItem: FC<MenuItemType> = (item) => {
switch (item.type) {
case 'link':
return (
<MenuItem render={<a href={item.href}>{item.label}</a>}>
{item.label}
</MenuItem>
)
case 'label':
return <MenuLabel>{item.label}</MenuLabel>
case 'separator':
return <MenuSeparator />
case 'group':
return (
<MenuGroup label={item.label}>
{item.content.map((item, index) => (
<RenderMenuItem
key={`group-${item.type}-${index.toString()}`}
{...item}
/>
))}
</MenuGroup>
)
}
}
const MainMenu: FC = () => {
const DialogMenuRef = useRef<HTMLDialogElement>(null)
return (
<>
<Button
type="button"
variant="ghost"
color="primary"
onClick={() => {
if (DialogMenuRef.current) {
DialogMenuRef.current.showModal()
}
}}
>
<FontAwesomeIcon icon={faBars} />
</Button>
<dialog
ref={DialogMenuRef}
className={menuDialogClass}
closedby="any"
>
<header className={menuHeaderContainerClass}>
<a
className={menuHeaderLogoLinkClass}
href="/"
>
<SrJuggernautLogo className={menuHeaderLogoClass} />
<span className={srOnlyClass}>Ir a la página principal</span>
</a>
<Button
type="button"
variant="ghost"
color="primary"
onClick={() => {
if (DialogMenuRef.current) {
DialogMenuRef.current.close()
}
}}
>
<FontAwesomeIcon icon={faTimes} />
</Button>
</header>
<Menu
render={<menu />}
className={css({
width: '100%'
})}
>
{menuContent.map((item, index) => (
<RenderMenuItem
key={`menu-${item.type}-${index.toString()}`}
{...item}
/>
))}
</Menu>
</dialog>
</>
)
}
export default MainMenu

View File

@@ -0,0 +1,22 @@
import { useRender } from '@base-ui/react/use-render'
import { cx } from '@styled-system/css'
import { type ButtonVariantProps, button } from '@styled-system/recipes/button'
import type { FC } from 'react'
import type { MergeOmitting } from '@/types/helpers'
export type ButtonProps = MergeOmitting<
useRender.ComponentProps<'button'>,
ButtonVariantProps
>
const Button: FC<ButtonProps> = ({ render, className, ...props }) => {
const [buttonArgs, otherProps] = button.splitVariantProps(props)
return useRender({
defaultTagName: 'button',
render,
props: { className: cx(button(buttonArgs), className), ...otherProps }
})
}
export default Button

107
src/components/ui/Menu.tsx Normal file
View File

@@ -0,0 +1,107 @@
import { useRender } from '@base-ui/react/use-render'
import { cx } from '@styled-system/css'
import { type MenuVariantProps, menu } from '@styled-system/recipes/menu'
import type { FC, ReactNode } from 'react'
import type { MergeOmitting } from '@/types/helpers'
export type MenuProps = MergeOmitting<
useRender.ComponentProps<'div'>,
MenuVariantProps
>
const Menu: FC<MenuProps> = ({ render, className, ...props }) => {
const [menuProps, allOther] = menu.splitVariantProps(props)
return useRender({
defaultTagName: 'div',
render,
props: { className: cx(menu(menuProps).container, className), ...allOther }
})
}
export default Menu
export type MenuItemProps = MergeOmitting<
useRender.ComponentProps<'button'>,
MenuVariantProps
>
export const MenuItem: FC<MenuItemProps> = ({
render,
className,
...props
}) => {
const [menuProps, allOther] = menu.splitVariantProps(props)
return useRender({
defaultTagName: 'button',
render,
props: { className: cx(menu(menuProps).item, className), ...allOther }
})
}
export type MenuLabelProps = MergeOmitting<
useRender.ComponentProps<'span'>,
MenuVariantProps
>
export const MenuLabel: FC<MenuLabelProps> = ({
render,
className,
...props
}) => {
const [menuProps, allOther] = menu.splitVariantProps(props)
return useRender({
defaultTagName: 'span',
render,
props: { className: cx(menu(menuProps).label, className), ...allOther }
})
}
export type MenuGroupProps = MergeOmitting<
useRender.ComponentProps<'div'>,
MenuVariantProps & { label: ReactNode }
>
export const MenuGroup: FC<MenuGroupProps> = ({
render,
children,
className,
label,
...props
}) => {
const [menuProps, allOther] = menu.splitVariantProps(props)
return useRender({
defaultTagName: 'div',
render,
props: {
className: cx(menu(menuProps).group, className),
children: (
<>
<span className={cx(menu(menuProps).label)}>{label}</span>
{children}
</>
),
...allOther
}
})
}
export type MenuSeparatorProps = MergeOmitting<
Omit<useRender.ComponentProps<'div'>, 'children'>,
MenuVariantProps
>
export const MenuSeparator: FC<MenuSeparatorProps> = ({
render,
className,
...props
}) => {
const [menuProps, allOther] = menu.splitVariantProps(props)
return useRender({
defaultTagName: 'div',
render,
props: { className: cx(menu(menuProps).separator, className), ...allOther }
})
}

View File

@@ -14,7 +14,7 @@ import { Route as IndexRouteImport } from './routes/index'
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
getParentRoute: () => rootRouteImport getParentRoute: () => rootRouteImport,
} as any) } as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
@@ -52,15 +52,14 @@ declare module '@tanstack/react-router' {
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute IndexRoute: IndexRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>() ._addFileTypes<FileRouteTypes>()
import type { createStart } from '@tanstack/react-start'
import type { getRouter } from './router.tsx' import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' { declare module '@tanstack/react-start' {
interface Register { interface Register {
ssr: true ssr: true

View File

@@ -1,4 +1,9 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
import ORBITRON from '@fontsource/orbitron/900.css?url'
import ROBOTO from '@fontsource-variable/roboto?url'
import { config } from '@fortawesome/fontawesome-svg-core'
import FONTAWESOME_STYLES from '@fortawesome/fontawesome-svg-core/styles.css?url'
import { import {
createRootRoute, createRootRoute,
HeadContent, HeadContent,
@@ -6,6 +11,12 @@ import {
Scripts Scripts
} from '@tanstack/react-router' } from '@tanstack/react-router'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import Footer from '@/components/layout/fragments/Footer'
import Header from '@/components/layout/fragments/Header'
import NotFound from '@/components/NotFound'
import GLOBAL_CSS from '@/styles/global.css?url'
config.autoAddCss = false
export const Route = createRootRoute({ export const Route = createRootRoute({
head: () => ({ head: () => ({
@@ -19,10 +30,60 @@ export const Route = createRootRoute({
}, },
{ {
title: 'Juggernaut Plays Blog' title: 'Juggernaut Plays Blog'
},
{
name: 'description',
content: 'Juego con cosas y luego hablo de ello.'
}
],
links: [
{
rel: 'stylesheet',
href: GLOBAL_CSS
},
{
rel: 'stylesheet',
href: ROBOTO
},
{
rel: 'stylesheet',
href: ORBITRON
},
{
rel: 'stylesheet',
href: FONTAWESOME_STYLES
},
{
rel: 'icon',
type: 'image/svg+xml',
href: '/favicon.svg'
},
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico'
},
{
rel: 'icon',
type: 'image/png',
sizes: '16x16',
href: '/favicon-16x16.png'
},
{
rel: 'icon',
type: 'image/png',
sizes: '32x32',
href: '/favicon-32x32.png'
},
{
rel: 'apple-touch-icon',
sizes: '180x180',
href: '/favicon-apple-touch.png'
} }
] ]
}), }),
component: RootComponent component: RootComponent,
notFoundComponent: NotFound
}) })
function RootComponent() { function RootComponent() {
@@ -40,7 +101,9 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
<HeadContent /> <HeadContent />
</head> </head>
<body> <body>
<Header />
{children} {children}
<Footer />
<Scripts /> <Scripts />
</body> </body>
</html> </html>

View File

@@ -1,7 +1,33 @@
import { css, cx } from '@styled-system/css'
import { skeleton } from '@styled-system/patterns'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import SrJuggernautLogo from '@/components/assets/SrJuggernautLogo'
import FullWidth from '@/components/layout/FullWidth'
const HomeRoute = () => { const HomeRoute = () => {
return <div>Home</div> return (
<FullWidth
className={cx(
skeleton({ duration: 4.5, variant: 'shimmerRight' }),
css({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
})
)}
>
<SrJuggernautLogo
className={css({
fill: 'neutral.12',
minWidth: '200px',
width: '100%',
maxWidth: { sm: 'breakpoint-sm' }
})}
/>
<h1>Work in progress</h1>
</FullWidth>
)
} }
export const Route = createFileRoute('/')({ export const Route = createFileRoute('/')({

1
src/styles/global.css Normal file
View File

@@ -0,0 +1 @@
@layer reset, base, tokens, recipes, utilities;

15
src/styles/srOnly.ts Normal file
View File

@@ -0,0 +1,15 @@
import { css } from '@styled-system/css'
const srOnlyClass = css({
position: 'absolute',
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
border: 0
})
export default srOnlyClass

19
src/styles/theme.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { ThemeConfig } from '@srjuggernaut-dev/srjuggernaut-panda-preset'
export const breakpoints = {
sm: 576,
md: 768,
lg: 992,
xl: 1200,
'2xl': 1400
} satisfies ThemeConfig['breakpoints']
export const themeConfig: ThemeConfig = {
neutral: 'slate',
colorVariation: { dark: true, alpha: false, p3: false },
includeColors: ['teal', 'slate'],
semanticColors: { primary: 'teal' },
breakpoints
}
export default themeConfig

5
src/types/helpers.ts Normal file
View File

@@ -0,0 +1,5 @@
export type MergeOmitting<ReplaceableType, ReplacerType> = Omit<
ReplaceableType,
keyof ReplacerType
> &
ReplacerType

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
// Environment setup & latest features // Environment setup & latest features
"lib": ["ESNext"], "lib": ["ESNext", "DOM"],
"target": "ES2022", "target": "ES2022",
"module": "ESNext", "module": "ESNext",
"moduleDetection": "force", "moduleDetection": "force",
@@ -30,7 +30,8 @@
// Paths // Paths
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"],
"@styled-system/*": ["./styled-system/*"]
} }
} }
} }

View File

@@ -1,8 +1,10 @@
import { tanstackStart } from '@tanstack/react-start/plugin/vite' import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import tsConfigPaths from 'vite-tsconfig-paths'
export default defineConfig({ export default defineConfig({
plugins: [tsConfigPaths(), tanstackStart(), react()] resolve: {
tsconfigPaths: true
},
plugins: [tanstackStart(), react()]
}) })