Skip to content

Commit

Permalink
add and integrate api routes
Browse files Browse the repository at this point in the history
  • Loading branch information
PssbleTrngle committed Aug 7, 2024
1 parent 9e76f5c commit 8b355c1
Show file tree
Hide file tree
Showing 21 changed files with 661 additions and 238 deletions.
File renamed without changes.
227 changes: 221 additions & 6 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions web/.prettierrc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** @type {import("prettier").Config} */
export default {
plugins: ['prettier-plugin-astro'],
semi: false,
tabWidth: 3,
printWidth: 120,
arrowParens: 'avoid',
singleQuote: true,
overrides: [
{
files: '*.astro',
options: {
parser: 'astro',
},
},
],
}
4 changes: 2 additions & 2 deletions web/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
18 changes: 9 additions & 9 deletions web/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
14 changes: 10 additions & 4 deletions web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ RUN mv web/public/icons pruned/public/icons
WORKDIR pruned
RUN pnpm run build

FROM nginx:alpine AS runtime
COPY web/nginx.conf /etc/nginx/nginx.conf
FROM node:20-slim AS runtime

COPY --from=builder /app/pruned/dist /usr/share/nginx/html
WORKDIR /app
COPY --from=builder /app/pruned/node_modules ./node_modules
COPY --from=builder /app/pruned/dist ./dist
COPY --from=builder /app/pruned/public ./public

ENV HOST=0.0.0.0
ENV PORT=80
EXPOSE 80

EXPOSE 80
CMD node ./dist/server/entry.mjs
7 changes: 6 additions & 1 deletion web/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import preact from '@astrojs/preact'
import { defineConfig } from 'astro/config'

import preact from '@astrojs/preact'
import node from '@astrojs/node'

// https://astro.build/config
export default defineConfig({
integrations: [preact()],
output: 'hybrid',
adapter: node({
mode: 'standalone',
}),
})
7 changes: 7 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"format": "prettier . --write",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.1",
"@astrojs/node": "^8.3.2",
"@astrojs/preact": "^3.5.1",
"@types/node": "^17.0.45",
"astro": "^4.13.0",
"debounce": "^2.1.0",
"fuse.js": "^7.0.0",
"preact": "^10.23.1",
"preact-fetching": "^1.0.0",
"typescript": "^5.5.4"
},
"devDependencies": {
"prettier-plugin-astro": "^0.14.1"
}
}
Binary file added web/src/assets/hole.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 36 additions & 77 deletions web/src/components/IconList.tsx
Original file line number Diff line number Diff line change
@@ -1,111 +1,70 @@
import Fuse, { type Expression, type FuseResult } from 'fuse.js'
import { useEffect, useMemo, useReducer } from 'preact/hooks'
import { type FuseResult } from 'fuse.js'
import { useQuery } from 'preact-fetching'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'preact/hooks'
import type { Icon } from '../types/Icon.ts'
import IconPanel from './IconPanel.tsx'

import debounce from 'debounce'
import './IconList.module.css'

type Props = {
icons: Icon[]
items: FuseResult<Icon>[]
query: string
limit: number
}

const MAX_ENTRIES = 250
const MIN_QUERY_LENGTH = 1

type Encodeable = { toString(): string }

function useQueryState<T extends Encodeable>(key: string, decode: (from: string) => T | null, defaultValue: T) {
function useQueryState<T extends Encodeable>(key: string, defaultValue: T) {
const state = useReducer((_: T, value: T) => {
const url = new URL(window.location.href)
url.searchParams.set(key, value.toString())
window.history.replaceState(value, value.toString(), url)
return value
}, defaultValue)

// Load query params on client
useEffect(() => {
const query = new URLSearchParams(window.location.search)
if (query.has(key)) {
const decoded = decode(query.get(key)!)
if (decoded !== null) state[1](decoded)
}
}, [])

return state
}

function toExpression(query: string): string | Expression {
const namespaceQueries =
query
.match(/@(["\^\$!\w]*)/g)
?.map(it => it.slice(1))
?.filter(it => it.length) ?? []

const idQueries = query
.replace(/@["\^\$!\w]*/g, '')
.split(/\s+/)
.filter(it => it.trim().length)

return {
$and: [...idQueries.map(search => ({ id: search })), ...namespaceQueries.map(search => ({ namespace: search }))],
}
type Page<T> = {
items: T[]
count: number
total: number
}

export default function IconList({ icons }: Props) {
const [query, setQuery] = useQueryState('q', it => it, '')
const [size] = useQueryState('s', parseInt, MAX_ENTRIES)

const unfiltered = useMemo(
() =>
icons.map<FuseResult<Icon>>((item, refIndex) => ({
item,
refIndex,
})),
[icons]
)
async function fetchItems(query: string, limit: number): Promise<Page<FuseResult<Icon>>> {
const response = await fetch(`/browse.json?includeMatches=true&query=${query}&limit=${limit}`)
if (!response.ok) throw new Error(response.statusText)
return await response.json()
}

const fuse = useMemo(
() =>
new Fuse(icons, {
keys: [
{
name: 'id',
weight: 10,
},
{
name: 'namespace',
weight: 1,
},
],
includeMatches: true,
minMatchCharLength: MIN_QUERY_LENGTH,
threshold: 0.25,
useExtendedSearch: true,
}),
[icons]
)
export function useLazyQuery<T>(data: T | undefined | null, initialData?: T) {
const [value, setValue] = useState(initialData)
useEffect(() => {
if (data) setValue(data)
}, [data])
return value
}

const filtered = useMemo(() => {
if (query.trim().length < MIN_QUERY_LENGTH) return unfiltered
return fuse.search(toExpression(query))
}, [query, icons])
export default function IconList(initial: Props) {
const [query, setQuery] = useQueryState('q', initial.query)
const [size] = useQueryState('s', initial.limit)
const setQueryDebounced = useMemo(() => debounce(setQuery, 250), [setQuery])

const sliced = useMemo(() => filtered.slice(0, size), [filtered, size])
const fetch = useCallback(() => fetchItems(query, size), [query, size])
const { data } = useQuery(`browse/${query}/${size}´`, fetch)
const items = useLazyQuery(data?.items, initial.items)

return (
<div>
<input
type='text'
name='search'
placeholder='Search...'
type="text"
name="search"
placeholder="Search..."
value={query}
onInput={e => setQuery(e.currentTarget.value)}
onInput={e => setQueryDebounced(e.currentTarget.value)}
/>
<ul>
{sliced.map(icon => (
<IconPanel {...icon} key={icon.item.url} />
))}
</ul>
<ul>{items?.map(icon => <IconPanel {...icon} key={icon.item.url} />)}</ul>
</div>
)
}
2 changes: 1 addition & 1 deletion web/src/components/IconPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function Highlighted({ children, match }: { match?: FuseResultMatch | undefined;
const firstMatch = indices[0]?.[0] ?? children.length

return [
<span key='start'>{children.slice(0, firstMatch)}</span>,
<span key="start">{children.slice(0, firstMatch)}</span>,
...indices.flatMap(([from, to], i, a) => {
const nextMatch = a[i + 1]?.[0] ?? children.length
return [
Expand Down
52 changes: 52 additions & 0 deletions web/src/components/StatusLayout.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
import hole from '../assets/hole.png'
import Layout from '../layouts/Layout.astro'
interface Props {
title: string
}
const { title } = Astro.props
---

<Layout title={title}>
<main class="centered">
<section class="hole centered" style={{ backgroundImage: `url(${hole.src})` }}>
<slot />
</section>
</main>
</Layout>

<style scoped>
main,
.hole {
height: calc(100dvh - 2em);
}

.centered {
display: grid;
place-content: center;
}

.hole :global(h1, h2) {
margin: 0;
text-align: center;
}

.hole :global(h1) {
font-size: 7rem;
}

.hole :global(h2) {
font-size: 1.5rem;
}

.hole {
aspect-ratio: 4 / 3;
max-width: 90dvw;

background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
</style>
Loading

0 comments on commit 8b355c1

Please sign in to comment.