8 Commits

Author SHA1 Message Date
9b48661a6a update readme 2026-03-13 10:37:47 +01:00
4916a2fc7b Merge branch 'projectpage' 2026-03-10 21:01:36 +01:00
b0fb481cf2 gitignore worktrees 2026-03-10 21:00:39 +01:00
495bd52c5b neon stack item 2026-03-10 20:59:46 +01:00
bf2085fcce project description 2026-03-10 20:57:09 +01:00
61e016d829 project description 2026-03-10 20:35:30 +01:00
e77eda0220 project page 2026-03-10 20:20:47 +01:00
fc4fab9478 fix adminwrap issue 2026-03-10 19:52:00 +01:00
16 changed files with 302 additions and 50 deletions

1
.gitignore vendored
View File

@@ -46,3 +46,4 @@ yarn-error.log*
.idea .idea
# clerk configuration (can include secrets) # clerk configuration (can include secrets)
/.clerk/ /.clerk/
.worktrees

View File

@@ -1,29 +1,13 @@
# Create T3 App # My Personal Website
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. ## Using:
## What's next? How do I make an app with this? - nextjs
- trpc
- neon
- uploadthing
- drizzle
- gsap
- openai
We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
- [Next.js](https://nextjs.org)
- [NextAuth.js](https://next-auth.js.org)
- [Prisma](https://prisma.io)
- [Drizzle](https://orm.drizzle.team)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
## Learn More
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
- [Documentation](https://create.t3.gg/)
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
## How do I deploy this?
Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.

View File

@@ -40,6 +40,7 @@
"@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@t3-oss/env-nextjs": "^0.13.10", "@t3-oss/env-nextjs": "^0.13.10",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"@tanstack/react-query-next-experimental": "^5.91.0", "@tanstack/react-query-next-experimental": "^5.91.0",
"@testing-library/user-event": "^14.6.1", "@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/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/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=="], "@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": ["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=="], "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
@@ -2571,6 +2574,8 @@
"shadcn/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "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=="], "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],

View File

@@ -54,6 +54,7 @@
"@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@t3-oss/env-nextjs": "^0.13.10", "@t3-oss/env-nextjs": "^0.13.10",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"@tanstack/react-query-next-experimental": "^5.91.0", "@tanstack/react-query-next-experimental": "^5.91.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",

View File

@@ -1,7 +1,10 @@
import { isAdmin } from "~/app/actions" // "use client"
// import { isAdmin } from "~/app/actions"
export default async function AdminWrap({children,}: Readonly<{ children: React.ReactNode }>) { import { useUser } from "@clerk/nextjs"
if (await isAdmin()) { 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 <>{children}</>
} }
return (<></>) return (<></>)

View File

@@ -1,6 +1,7 @@
"use client"
import Link from "next/link" import Link from "next/link"
import AdminWrap from "./AdminWrap" 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 { Button } from "~/components/ui/button"
import { ThemeSwitch } from "./ThemeSwitch" import { ThemeSwitch } from "./ThemeSwitch"
@@ -39,6 +40,7 @@ export default function TopNav() {
</Button> </Button>
</Show> </Show>
<ThemeSwitch /> <ThemeSwitch />
<ClerkLoaded>
<Show when="signed-in"> <Show when="signed-in">
<Button asChild className="flex h-10 lg:h-full cursor-pointer lg:w-20 content-center" variant={"outline"}> <Button asChild className="flex h-10 lg:h-full cursor-pointer lg:w-20 content-center" variant={"outline"}>
<div> <div>
@@ -46,6 +48,7 @@ export default function TopNav() {
</div> </div>
</Button> </Button>
</Show> </Show>
</ClerkLoaded>
</div> </div>
</nav> </nav>
</div> </div>

View File

@@ -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 }).length > 0 ? (
<> <>
{entires[0].filter((e) => { return e.categoryId == cat.id }).map((entry) => ( {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" />
))} ))}
</> </>
) : (<></>) ) : (<></>)

View File

@@ -8,19 +8,23 @@ import type { IterableElement } from 'type-fest'
import { entitySchemas, makeOnSuccess } from "~/lib/utils"; import { entitySchemas, makeOnSuccess } from "~/lib/utils";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { RouterOutputs } from '~/server/routers/_app'; 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 { FormScaffold } from '~/app/_components/Form/Components';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { useTheme } from 'next-themes';
import { makeUseRelationShipWithNameIndex } from '~/lib/hooks'; import { makeUseRelationShipWithNameIndex } from '~/lib/hooks';
import { FormMutationContextProvider } from '~/app/_components/Form/Components/MutationProvider'; import { FormMutationContextProvider } from '~/app/_components/Form/Components/MutationProvider';
export default function CreateUpdateProjectForm(params: { className?: string, entity?: IterableElement<RouterOutputs['project']['select']> }) { 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 [id, setId] = useState<string | undefined>(params.entity ? params.entity.id : undefined)
const { theme } = useTheme()
const schemas = entitySchemas('project') 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 { 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>>({ const form = useForm<z.infer<typeof schemas.insert>>({
resolver: zodResolver(schemas.insert), resolver: zodResolver(schemas.insert),
defaultValues: { defaultValues: {
id: id ? id : crypto.randomUUID(), 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 : "", stackId: params.entity ? params.entity.stackId : stacksSuccess ? stacks?.at(0)?.id : "",
releaseStatus: params.entity ? params.entity.releaseStatus : "unreleased", releaseStatus: params.entity ? params.entity.releaseStatus : "unreleased",
releaseLink: params.entity ? params.entity.releaseLink : "", releaseLink: params.entity ? params.entity.releaseLink : "",
@@ -64,6 +68,7 @@ export default function CreateUpdateProjectForm(params: { className?: string, en
} }
</SelectFormField> </SelectFormField>
<TextInputFormField control={form.control} name='title' label='Title' /> <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' > <SelectFormField control={form.control} name='sourceType' label='Source Type' defaultValue={'open'} placeholder='open' >
<SelectItem value="open"> open </SelectItem> <SelectItem value="open"> open </SelectItem>
<SelectItem value="closed"> closed </SelectItem> <SelectItem value="closed"> closed </SelectItem>

View File

@@ -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 }).length > 0 ? (
<> <>
{techStacks[0].filter((e) => { return e.id == project.stackId }).map((stack) => ( {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"/>
))} ))}
</> </>
) : (<></>) ) : (<></>)

View File

@@ -1,12 +1,93 @@
'use client' '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 Page() { export default function ProjectsPage() {
const pathName = usePathname() const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery();
if (isLoading) {
return ( return (
<div> <div className="flex justify-center items-center min-h-[200px] text-muted-foreground">
{pathName} Loading...
</div> </div>
) );
}
if (!projects?.length) {
return (
<div className="flex justify-center items-center min-h-[200px] text-muted-foreground">
No projects yet.
</div>
);
}
return (
<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>
);
} }

View 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>
);
}

View File

@@ -38,6 +38,7 @@ export const env = createEnv({
* `NEXT_PUBLIC_`. * `NEXT_PUBLIC_`.
*/ */
client: { client: {
NEXT_PUBLIC_ADMIN_USER_CLERK_ID: z.string(),
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string() NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string()
// NEXT_PUBLIC_CLIENTVAR: 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_URL_NO_SSL: process.env.POSTGRES_URL_NO_SSL,
POSTGRES_PRISMA_URL: process.env.POSTGRES_PRISMA_URL, POSTGRES_PRISMA_URL: process.env.POSTGRES_PRISMA_URL,
ADMIN_USER_CLERK_ID: process.env.ADMIN_USER_CLERK_ID, 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, CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,

View File

@@ -4,6 +4,7 @@ import { env } from "~/env";
const isTenantAdminRoute = createRouteMatcher(['/admin(.*)']) const isTenantAdminRoute = createRouteMatcher(['/admin(.*)'])
export default clerkMiddleware(async (auth,req) => { export default clerkMiddleware(async (auth,req) => {
if (isTenantAdminRoute(req)) { if (isTenantAdminRoute(req)) {
console.log("running clerk middleware");
let userid = (await auth()).userId let userid = (await auth()).userId
if (userid != env.ADMIN_USER_CLERK_ID) { if (userid != env.ADMIN_USER_CLERK_ID) {
await auth.protect() await auth.protect()

View File

@@ -55,13 +55,14 @@ export const cvEntryRelations = relations(cvEntry, ({one}) => ({
export const sourceTypeEnum = pgEnum('source_type',['open','closed']) export const sourceTypeEnum = pgEnum('source_type',['open','closed'])
export const releaseStatus = pgEnum('release_status',['released','unreleased']) 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( export const project = createTable(
"project", "project",
(d) => ({ (d) => ({
id: d.uuid().primaryKey().notNull(), id: d.uuid().primaryKey().notNull(),
title: d.varchar({length: 50}).notNull(), title: d.varchar({length: 50}).notNull(),
description: d.text(),
sourceType: sourceTypeEnum(), sourceType: sourceTypeEnum(),
sourceLink: d.varchar({length: 200}), sourceLink: d.varchar({length: 200}),
releaseStatus: releaseStatus(), releaseStatus: releaseStatus(),

View File

@@ -1,4 +1,12 @@
import { router } from "~/server/trpc"; import { publicProcedure, router } from "~/server/trpc";
import { db } from "~/server/db";
export const projectRouter = router({ export const projectRouter = router({
listWithStack: publicProcedure.query(async () => {
return db.query.project.findMany({
with: {
techStack: true,
},
});
}),
}); });

View File

@@ -1,7 +1,8 @@
import type { Config } from "tailwindcss" import type { Config } from "tailwindcss"
const config = { const config = {
plugins: [ plugins: [
require("tailwindcss-motion") require("tailwindcss-motion"),
require("@tailwindcss/typography")
] ]
} satisfies Config } satisfies Config