mirror of
https://github.com/Kludex/awesome-fastapi-projects.git
synced 2025-05-14 13:17:04 +00:00
Add a draft of data table and display all the repos
This commit is contained in:
parent
3ba2723368
commit
7bc2e6bb05
4
.gitignore
vendored
4
.gitignore
vendored
@ -14,7 +14,6 @@ dist/
|
|||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
@ -158,3 +157,6 @@ cython_debug/
|
|||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Ruff cache
|
||||||
|
.ruff_cache/
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""The application-level conftest."""
|
"""The application-level conftest."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
from collections.abc import AsyncGenerator, Generator
|
from collections.abc import AsyncGenerator, Generator
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
@ -43,11 +44,8 @@ def event_loop(
|
|||||||
An event loop is destroyed at the end of the test session.
|
An event loop is destroyed at the end of the test session.
|
||||||
https://docs.pytest.org/en/6.2.x/fixture.html#fixture-scopes
|
https://docs.pytest.org/en/6.2.x/fixture.html#fixture-scopes
|
||||||
"""
|
"""
|
||||||
loop = asyncio.get_event_loop_policy().get_event_loop()
|
with contextlib.closing(loop := asyncio.get_event_loop_policy().get_event_loop()):
|
||||||
try:
|
|
||||||
yield loop
|
yield loop
|
||||||
finally:
|
|
||||||
loop.close()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Create index.json file from database."""
|
"""Create index.json file from database."""
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import sqlalchemy.orm
|
import sqlalchemy.orm
|
||||||
@ -10,7 +11,8 @@ from app.database import Repo
|
|||||||
from app.models import RepoDetail
|
from app.models import RepoDetail
|
||||||
from app.uow import async_session_uow
|
from app.uow import async_session_uow
|
||||||
|
|
||||||
INDEX_PATH = Path(__file__).parent.parent / "index.json"
|
#: The path to the index.json file.
|
||||||
|
INDEX_PATH: Final[Path] = Path(__file__).parent.parent / "index.json"
|
||||||
|
|
||||||
|
|
||||||
async def create_index() -> None:
|
async def create_index() -> None:
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/react-table": "^8.9.3",
|
||||||
"@types/node": "20.5.1",
|
"@types/node": "20.5.1",
|
||||||
"@types/react": "18.2.20",
|
"@types/react": "18.2.20",
|
||||||
"@types/react-dom": "18.2.7",
|
"@types/react-dom": "18.2.7",
|
||||||
@ -25,6 +26,7 @@
|
|||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwindcss": "3.3.3",
|
"tailwindcss": "3.3.3",
|
||||||
"tailwindcss-animate": "^1.0.6",
|
"tailwindcss-animate": "^1.0.6",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6",
|
||||||
|
"zod": "^3.22.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
36
frontend/pnpm-lock.yaml
generated
36
frontend/pnpm-lock.yaml
generated
@ -5,6 +5,9 @@ settings:
|
|||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@tanstack/react-table":
|
||||||
|
specifier: ^8.9.3
|
||||||
|
version: 8.9.3(react-dom@18.2.0)(react@18.2.0)
|
||||||
"@types/node":
|
"@types/node":
|
||||||
specifier: 20.5.1
|
specifier: 20.5.1
|
||||||
version: 20.5.1
|
version: 20.5.1
|
||||||
@ -56,6 +59,9 @@ dependencies:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: 5.1.6
|
specifier: 5.1.6
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
|
zod:
|
||||||
|
specifier: ^3.22.2
|
||||||
|
version: 3.22.2
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
/@aashutoshrathi/word-wrap@1.2.6:
|
/@aashutoshrathi/word-wrap@1.2.6:
|
||||||
@ -377,6 +383,29 @@ packages:
|
|||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/react-table@8.9.3(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-Ng9rdm3JPoSCi6cVZvANsYnF+UoGVRxflMb270tVj0+LjeT/ZtZ9ckxF6oLPLcKesza6VKBqtdF9mQ+vaz24Aw==,
|
||||||
|
}
|
||||||
|
engines: { node: ">=12" }
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16"
|
||||||
|
react-dom: ">=16"
|
||||||
|
dependencies:
|
||||||
|
"@tanstack/table-core": 8.9.3
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/table-core@8.9.3:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-NpHZBoHTfqyJk0m/s/+CSuAiwtebhYK90mDuf5eylTvgViNOujiaOaxNDxJkQQAsVvHWZftUGAx1EfO1rkKtLg==,
|
||||||
|
}
|
||||||
|
engines: { node: ">=12" }
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/json5@0.0.29:
|
/@types/json5@0.0.29:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -3640,3 +3669,10 @@ packages:
|
|||||||
integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==,
|
integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==,
|
||||||
}
|
}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/zod@3.22.2:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==,
|
||||||
|
}
|
||||||
|
dev: false
|
||||||
|
17
frontend/src/app/columns.tsx
Normal file
17
frontend/src/app/columns.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Repo } from "@/lib/schemas";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
|
||||||
|
export const columns: ColumnDef<Repo>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "url",
|
||||||
|
header: "Link",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "description",
|
||||||
|
header: "Description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "stars",
|
||||||
|
header: "Stars",
|
||||||
|
},
|
||||||
|
];
|
80
frontend/src/app/data-table.tsx
Normal file
80
frontend/src/app/data-table.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
|
interface DataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
data: TData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable<TData, TValue>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -5,64 +5,47 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary: 142.1 76.2% 36.3%;
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
--secondary: 210 40% 96.1%;
|
--muted: 240 4.8% 95.9%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
--muted: 210 40% 96.1%;
|
--accent-foreground: 240 5.9% 10%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
--border: 214.3 31.8% 91.4%;
|
--input: 240 5.9% 90%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--ring: 142.1 76.2% 36.3%;
|
||||||
--ring: 222.2 84% 4.9%;
|
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 20 14.3% 4.1%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 0 0% 95%;
|
||||||
|
--card: 24 9.8% 10%;
|
||||||
--card: 222.2 84% 4.9%;
|
--card-foreground: 0 0% 95%;
|
||||||
--card-foreground: 210 40% 98%;
|
--popover: 0 0% 9%;
|
||||||
|
--popover-foreground: 0 0% 95%;
|
||||||
--popover: 222.2 84% 4.9%;
|
--primary: 142.1 70.6% 45.3%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--primary-foreground: 144.9 80.4% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
--primary: 210 40% 98%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--muted: 0 0% 15%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--accent: 12 6.5% 15.1%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
|
||||||
--accent-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 85.7% 97.3%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
--border: 217.2 32.6% 17.5%;
|
--input: 240 3.7% 15.9%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--ring: 142.4 71.8% 29.2%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,113 +1,12 @@
|
|||||||
import Image from "next/image";
|
import { loadIndexServerOnly } from "@/lib/load-index-server-only";
|
||||||
|
import { DataTable } from "./data-table";
|
||||||
|
import { columns } from "./columns";
|
||||||
|
|
||||||
export default function Home() {
|
export default async function Home() {
|
||||||
|
const { repos } = await loadIndexServerOnly();
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
<section className="py-10">
|
||||||
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
|
<DataTable columns={columns} data={repos} />
|
||||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
</section>
|
||||||
Get started by editing
|
|
||||||
<code className="font-mono font-bold">src/app/page.tsx</code>
|
|
||||||
</p>
|
|
||||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
|
|
||||||
<a
|
|
||||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
|
||||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
By{" "}
|
|
||||||
<Image
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel Logo"
|
|
||||||
className="dark:invert"
|
|
||||||
width={100}
|
|
||||||
height={24}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
|
|
||||||
<Image
|
|
||||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js Logo"
|
|
||||||
width={180}
|
|
||||||
height={37}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
|
||||||
Docs{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
|
||||||
Find in-depth information about Next.js features and API.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
|
||||||
Learn{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
|
||||||
Learn about Next.js in an interactive course with quizzes!
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
|
||||||
Templates{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
|
||||||
Explore the Next.js 13 playground.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
|
||||||
Deploy{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
|
||||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
114
frontend/src/components/ui/table.tsx
Normal file
114
frontend/src/components/ui/table.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Table = React.forwardRef<
|
||||||
|
HTMLTableElement,
|
||||||
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
Table.displayName = "Table";
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
|
));
|
||||||
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn("bg-primary font-medium text-primary-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableFooter.displayName = "TableFooter";
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableCaption.displayName = "TableCaption";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
};
|
30
frontend/src/lib/load-index-server-only.ts
Normal file
30
frontend/src/lib/load-index-server-only.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { cache } from "react";
|
||||||
|
import "server-only";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import { indexSchema } from "./schemas";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
export const INDEX_FILE_PATH = path.normalize(
|
||||||
|
path.join(__dirname, "..", "..", "..", "..", "index.json"),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const preload = () => {
|
||||||
|
void loadIndexServerOnly();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadIndexServerOnly = cache(async () => {
|
||||||
|
try {
|
||||||
|
const indexData = JSON.parse(
|
||||||
|
await fs.promises.readFile(INDEX_FILE_PATH, "utf-8"),
|
||||||
|
);
|
||||||
|
return await indexSchema.parseAsync(indexData);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ZodError) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse the repos index: ${JSON.stringify(err.format())}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to load the repos index: ${err}`);
|
||||||
|
}
|
||||||
|
});
|
24
frontend/src/lib/schemas.ts
Normal file
24
frontend/src/lib/schemas.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const dependencySchema = z.object({
|
||||||
|
id: z.number().min(0),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const repoSchema = z.object({
|
||||||
|
id: z.number().min(0),
|
||||||
|
url: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
stars: z.number().min(0),
|
||||||
|
source_graph_repo_id: z.number().min(0),
|
||||||
|
dependencies: z.array(dependencySchema),
|
||||||
|
last_checked_revision: z.nullable(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const indexSchema = z.object({
|
||||||
|
repos: z.array(repoSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Dependency = z.infer<typeof dependencySchema>;
|
||||||
|
export type Repo = z.infer<typeof repoSchema>;
|
||||||
|
export type Index = z.infer<typeof indexSchema>;
|
622
index.json
622
index.json
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user