Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4916a2fc7b | |||
| b0fb481cf2 | |||
| 495bd52c5b | |||
| bf2085fcce | |||
| 61e016d829 | |||
| e77eda0220 | |||
| fc4fab9478 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -46,3 +46,4 @@ yarn-error.log*
|
||||
.idea
|
||||
# clerk configuration (can include secrets)
|
||||
/.clerk/
|
||||
.worktrees
|
||||
|
||||
7
bun.lock
7
bun.lock
@@ -40,6 +40,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@t3-oss/env-nextjs": "^0.13.10",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-query-next-experimental": "^5.91.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -777,6 +778,8 @@
|
||||
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="],
|
||||
|
||||
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
|
||||
@@ -1903,7 +1906,7 @@
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
||||
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
|
||||
|
||||
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
|
||||
|
||||
@@ -2571,6 +2574,8 @@
|
||||
|
||||
"shadcn/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||
|
||||
"shadcn/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
||||
|
||||
"shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@t3-oss/env-nextjs": "^0.13.10",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-query-next-experimental": "^5.91.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { isAdmin } from "~/app/actions"
|
||||
|
||||
export default async function AdminWrap({children,}: Readonly<{ children: React.ReactNode }>) {
|
||||
if (await isAdmin()) {
|
||||
// "use client"
|
||||
// import { isAdmin } from "~/app/actions"
|
||||
import { useUser } from "@clerk/nextjs"
|
||||
import { env } from "~/env"
|
||||
export default function AdminWrap({children,}: Readonly<{ children: React.ReactNode }>) {
|
||||
const user = useUser();
|
||||
if (user.isSignedIn && user.user.id == env.NEXT_PUBLIC_ADMIN_USER_CLERK_ID) {
|
||||
return <>{children}</>
|
||||
}
|
||||
return (<></>)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
import Link from "next/link"
|
||||
import AdminWrap from "./AdminWrap"
|
||||
import { Show, SignInButton, SignOutButton, SignUpButton, UserButton } from "@clerk/nextjs"
|
||||
import { ClerkLoaded, Show, SignInButton, SignOutButton, SignUpButton, UserButton } from "@clerk/nextjs"
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { ThemeSwitch } from "./ThemeSwitch"
|
||||
|
||||
@@ -39,13 +40,15 @@ export default function TopNav() {
|
||||
</Button>
|
||||
</Show>
|
||||
<ThemeSwitch />
|
||||
<Show when="signed-in">
|
||||
<Button asChild className="flex h-10 lg:h-full cursor-pointer lg:w-20 content-center" variant={"outline"}>
|
||||
<div>
|
||||
<UserButton />
|
||||
</div>
|
||||
</Button>
|
||||
</Show>
|
||||
<ClerkLoaded>
|
||||
<Show when="signed-in">
|
||||
<Button asChild className="flex h-10 lg:h-full cursor-pointer lg:w-20 content-center" variant={"outline"}>
|
||||
<div>
|
||||
<UserButton />
|
||||
</div>
|
||||
</Button>
|
||||
</Show>
|
||||
</ClerkLoaded>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function CvPage() {
|
||||
entires[0].filter((e) => { return e.categoryId == cat.id }).length > 0 ? (
|
||||
<>
|
||||
{entires[0].filter((e) => { return e.categoryId == cat.id }).map((entry) => (
|
||||
<CollapsibleForm entityName="Entry" form={CreateUpdateCvEntryForm} entity={entry} entityLabelIndex="title" />
|
||||
<CollapsibleForm key={entry.id} entityName="Entry" form={CreateUpdateCvEntryForm} entity={entry} entityLabelIndex="title" />
|
||||
))}
|
||||
</>
|
||||
) : (<></>)
|
||||
|
||||
@@ -8,19 +8,23 @@ import type { IterableElement } from 'type-fest'
|
||||
import { entitySchemas, makeOnSuccess } from "~/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { RouterOutputs } from '~/server/routers/_app';
|
||||
import { SelectFormField, TextInputFormField } from '~/app/_components/Form/Fields'
|
||||
import { SelectFormField, TextInputFormField, MdeFormField } from '~/app/_components/Form/Fields'
|
||||
import { FormScaffold } from '~/app/_components/Form/Components';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { makeUseRelationShipWithNameIndex } from '~/lib/hooks';
|
||||
import { FormMutationContextProvider } from '~/app/_components/Form/Components/MutationProvider';
|
||||
export default function CreateUpdateProjectForm(params: { className?: string, entity?: IterableElement<RouterOutputs['project']['select']> }) {
|
||||
const [id, setId] = useState<string | undefined>(params.entity ? params.entity.id : undefined)
|
||||
const { theme } = useTheme()
|
||||
const schemas = entitySchemas('project')
|
||||
const { data: stacks, id: stackId, name: stackName, success: stacksSuccess, error: stackError } = makeUseRelationShipWithNameIndex('stackItems')(trpc.techStack.select.useQuery({}), id, (items) => { return items ? items.join('-') : "" })
|
||||
const form = useForm<z.infer<typeof schemas.insert>>({
|
||||
resolver: zodResolver(schemas.insert),
|
||||
defaultValues: {
|
||||
id: id ? id : crypto.randomUUID(),
|
||||
title: params.entity ? params.entity.title : "",
|
||||
description: params.entity ? params.entity.description : "",
|
||||
stackId: params.entity ? params.entity.stackId : stacksSuccess ? stacks?.at(0)?.id : "",
|
||||
releaseStatus: params.entity ? params.entity.releaseStatus : "unreleased",
|
||||
releaseLink: params.entity ? params.entity.releaseLink : "",
|
||||
@@ -64,6 +68,7 @@ export default function CreateUpdateProjectForm(params: { className?: string, en
|
||||
}
|
||||
</SelectFormField>
|
||||
<TextInputFormField control={form.control} name='title' label='Title' />
|
||||
<MdeFormField control={form.control} name='description' label='Description' dataColorMode={(theme as "dark" | "light") ?? "dark"} />
|
||||
<SelectFormField control={form.control} name='sourceType' label='Source Type' defaultValue={'open'} placeholder='open' >
|
||||
<SelectItem value="open"> open </SelectItem>
|
||||
<SelectItem value="closed"> closed </SelectItem>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function ProjectList() {
|
||||
techStacks[0].filter((e) => { return e.id == project.stackId }).length > 0 ? (
|
||||
<>
|
||||
{techStacks[0].filter((e) => { return e.id == project.stackId }).map((stack) => (
|
||||
<CollapsibleForm entityName="Stack" form={CreateUpdateStackForm} entity={stack} entityLabelIndex="stackItems"/>
|
||||
<CollapsibleForm key={stack.id} entityName="Stack" form={CreateUpdateStackForm} entity={stack} entityLabelIndex="stackItems"/>
|
||||
))}
|
||||
</>
|
||||
) : (<></>)
|
||||
|
||||
@@ -1,12 +1,93 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import { trpc } from "~/app/_trpc/Client";
|
||||
import * as Card from "~/components/ui/card";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { StackBadge } from "~/components/StackBadge";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[200px] text-muted-foreground">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!projects?.length) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[200px] text-muted-foreground">
|
||||
No projects yet.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const pathName = usePathname()
|
||||
return (
|
||||
<div>
|
||||
{pathName}
|
||||
<div className="w-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
|
||||
{projects.map((project) => (
|
||||
<Card.Card key={project.id}>
|
||||
<Card.CardHeader>
|
||||
<div className="flex items-start justify-between gap-2 flex-wrap">
|
||||
<Card.CardTitle>{project.title}</Card.CardTitle>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{project.sourceType && (
|
||||
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
|
||||
{project.sourceType === "open" ? "Open Source" : "Closed Source"}
|
||||
</Badge>
|
||||
)}
|
||||
{project.releaseStatus && (
|
||||
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
|
||||
{project.releaseStatus === "released" ? "Released" : "Unreleased"}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card.CardHeader>
|
||||
{(project.description || project.sourceLink || project.releaseLink || project.techStack?.stackItems?.length) && (
|
||||
<Card.CardContent className="flex flex-col gap-3">
|
||||
{project.description && (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground">
|
||||
<Markdown>{project.description}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
{(project.sourceLink || project.releaseLink) && (
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
{project.sourceLink && (
|
||||
<a
|
||||
href={project.sourceLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
|
||||
>
|
||||
Source
|
||||
</a>
|
||||
)}
|
||||
{project.releaseLink && (
|
||||
<a
|
||||
href={project.releaseLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
|
||||
>
|
||||
Live
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{project.techStack.stackItems.map((item) => (
|
||||
<StackBadge key={item} item={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card.CardContent>
|
||||
)}
|
||||
</Card.Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
156
src/components/StackBadge.tsx
Normal file
156
src/components/StackBadge.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
|
||||
interface SvglIcon {
|
||||
light: string;
|
||||
dark: string;
|
||||
}
|
||||
|
||||
const STACK_META: Record<string, { label: string; icon?: SvglIcon }> = {
|
||||
drizzle: {
|
||||
label: "Drizzle ORM",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/drizzle-orm_light.svg",
|
||||
dark: "https://svgl.app/library/drizzle-orm_dark.svg",
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
label: "PostgreSQL",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/postgresql.svg",
|
||||
dark: "https://svgl.app/library/postgresql.svg",
|
||||
},
|
||||
},
|
||||
nextjs: {
|
||||
label: "Next.js",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/nextjs_icon_dark.svg",
|
||||
dark: "https://svgl.app/library/nextjs_icon_dark.svg",
|
||||
},
|
||||
},
|
||||
react: {
|
||||
label: "React",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/react_light.svg",
|
||||
dark: "https://svgl.app/library/react_dark.svg",
|
||||
},
|
||||
},
|
||||
servercomponents: { label: "Server Components" },
|
||||
php: {
|
||||
label: "PHP",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/php.svg",
|
||||
dark: "https://svgl.app/library/php_dark.svg",
|
||||
},
|
||||
},
|
||||
laravel: {
|
||||
label: "Laravel",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/laravel.svg",
|
||||
dark: "https://svgl.app/library/laravel.svg",
|
||||
},
|
||||
},
|
||||
reactnative: {
|
||||
label: "React Native",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/react_light.svg",
|
||||
dark: "https://svgl.app/library/react_dark.svg",
|
||||
},
|
||||
},
|
||||
"react-native": {
|
||||
label: "React Native",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/react_light.svg",
|
||||
dark: "https://svgl.app/library/react_dark.svg",
|
||||
},
|
||||
},
|
||||
expo: {
|
||||
label: "Expo",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/expo.svg",
|
||||
dark: "https://svgl.app/library/expo.svg",
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
label: "MySQL",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/mysql-icon-light.svg",
|
||||
dark: "https://svgl.app/library/mysql-icon-dark.svg",
|
||||
},
|
||||
},
|
||||
nginx: {
|
||||
label: "Nginx",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/nginx.svg",
|
||||
dark: "https://svgl.app/library/nginx.svg",
|
||||
},
|
||||
},
|
||||
protobuf: { label: "Protobuf" },
|
||||
grpc: { label: "gRPC" },
|
||||
java: {
|
||||
label: "Java",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/java.svg",
|
||||
dark: "https://svgl.app/library/java.svg",
|
||||
},
|
||||
},
|
||||
graalvm: { label: "GraalVM" },
|
||||
spring: {
|
||||
label: "Spring",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/spring.svg",
|
||||
dark: "https://svgl.app/library/spring.svg",
|
||||
},
|
||||
},
|
||||
aws: {
|
||||
label: "AWS",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/aws_light.svg",
|
||||
dark: "https://svgl.app/library/aws_dark.svg",
|
||||
},
|
||||
},
|
||||
s3: { label: "Amazon S3" },
|
||||
linux: {
|
||||
label: "Linux",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/linux.svg",
|
||||
dark: "https://svgl.app/library/linux.svg",
|
||||
},
|
||||
},
|
||||
debian: { label: "Debian" },
|
||||
htmx: { label: "HTMX" },
|
||||
neon: {
|
||||
label: "Neon",
|
||||
icon: {
|
||||
light: "https://svgl.app/library/neon.svg",
|
||||
dark: "https://svgl.app/library/neon.svg",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function StackBadge({ item }: { item: string }) {
|
||||
const meta = STACK_META[item] ?? { label: item };
|
||||
|
||||
return (
|
||||
<Badge variant="outline">
|
||||
{meta.icon && (
|
||||
<>
|
||||
<img
|
||||
src={meta.icon.light}
|
||||
alt=""
|
||||
width={12}
|
||||
height={12}
|
||||
className="dark:hidden shrink-0"
|
||||
/>
|
||||
<img
|
||||
src={meta.icon.dark}
|
||||
alt=""
|
||||
width={12}
|
||||
height={12}
|
||||
className="hidden dark:block shrink-0"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{meta.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -38,6 +38,7 @@ export const env = createEnv({
|
||||
* `NEXT_PUBLIC_`.
|
||||
*/
|
||||
client: {
|
||||
NEXT_PUBLIC_ADMIN_USER_CLERK_ID: z.string(),
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string()
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||
},
|
||||
@@ -63,6 +64,7 @@ export const env = createEnv({
|
||||
POSTGRES_URL_NO_SSL: process.env.POSTGRES_URL_NO_SSL,
|
||||
POSTGRES_PRISMA_URL: process.env.POSTGRES_PRISMA_URL,
|
||||
ADMIN_USER_CLERK_ID: process.env.ADMIN_USER_CLERK_ID,
|
||||
NEXT_PUBLIC_ADMIN_USER_CLERK_ID: process.env.NEXT_PUBLIC_ADMIN_USER_CLERK_ID,
|
||||
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { env } from "~/env";
|
||||
const isTenantAdminRoute = createRouteMatcher(['/admin(.*)'])
|
||||
export default clerkMiddleware(async (auth,req) => {
|
||||
if (isTenantAdminRoute(req)) {
|
||||
console.log("running clerk middleware");
|
||||
let userid = (await auth()).userId
|
||||
if (userid != env.ADMIN_USER_CLERK_ID) {
|
||||
await auth.protect()
|
||||
|
||||
@@ -55,13 +55,14 @@ export const cvEntryRelations = relations(cvEntry, ({one}) => ({
|
||||
|
||||
export const sourceTypeEnum = pgEnum('source_type',['open','closed'])
|
||||
export const releaseStatus = pgEnum('release_status',['released','unreleased'])
|
||||
export const stackItemEnum = pgEnum('stack_item',['drizzle','postgres','nextjs','react','servercomponents','php','laravel','reactnative','expo','mysql','nginx','protobuf','grpc'])
|
||||
export const stackItemEnum = pgEnum('stack_item',['drizzle','postgres','nextjs','react','servercomponents','php','laravel','reactnative','expo','mysql','nginx','protobuf','grpc','java','graalvm','spring','aws','s3','react-native','linux','debian','htmx','neon'])
|
||||
|
||||
export const project = createTable(
|
||||
"project",
|
||||
(d) => ({
|
||||
id: d.uuid().primaryKey().notNull(),
|
||||
title: d.varchar({length: 50}).notNull(),
|
||||
description: d.text(),
|
||||
sourceType: sourceTypeEnum(),
|
||||
sourceLink: d.varchar({length: 200}),
|
||||
releaseStatus: releaseStatus(),
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { router } from "~/server/trpc";
|
||||
import { publicProcedure, router } from "~/server/trpc";
|
||||
import { db } from "~/server/db";
|
||||
|
||||
export const projectRouter = router({
|
||||
listWithStack: publicProcedure.query(async () => {
|
||||
return db.query.project.findMany({
|
||||
with: {
|
||||
techStack: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Config } from "tailwindcss"
|
||||
const config = {
|
||||
plugins: [
|
||||
require("tailwindcss-motion")
|
||||
require("tailwindcss-motion"),
|
||||
require("@tailwindcss/typography")
|
||||
]
|
||||
} satisfies Config
|
||||
|
||||
|
||||
Reference in New Issue
Block a user