9 Commits

Author SHA1 Message Date
10b3f989c8 log userid 2026-03-30 17:06:15 +02:00
348ed790e2 Admin, cant 'use server' 2026-03-30 14:34:28 +02:00
c7de58a4b8 Admin sidebar cant be async 2026-03-30 14:28:45 +02:00
a51b313aba force admin dynamic 2026-03-30 14:23:57 +02:00
363a91dd7d Merge branch 'projectpage' 2026-03-30 14:14:30 +02:00
dfaba3a24e animation stuff 2026-03-30 14:13:04 +02:00
9c5aec01e0 fix create techstack form 2026-03-14 18:40:23 +01:00
03399de14f Merge branch 'musicpage' 2026-03-14 18:38:10 +01:00
9b48661a6a update readme 2026-03-13 10:37:47 +01:00
14 changed files with 227 additions and 134 deletions

1
.gitignore vendored
View File

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

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

@@ -1,27 +1,38 @@
import { useGSAP } from "@gsap/react"; import { useGSAP } from "@gsap/react";
import { useRef, type ReactNode } from "react"; import { useRef, type HTMLAttributes, type ReactNode } from "react";
import { useGsapContext } from "~/app/_providers/GsapProvicer"; import { useGsapContext } from "~/app/_providers/GsapProvicer";
import { SplitText } from "gsap/SplitText"; import { SplitText } from "gsap/SplitText";
import gsap from 'gsap' import gsap from 'gsap'
const AnimateTextIn = ({children,animation="type",position}:{children:ReactNode,animation?:"type"|"slide",position:gsap.Position}) => { import { cn } from "~/lib/utils";
const AnimateTextIn = ({
children,
animation = "type",
position,
className
}: {
children: ReactNode,
animation?: "type" | "slide",
position: gsap.Position,
className?:HTMLAttributes<HTMLDivElement>['className']
}) => {
const el = useRef<HTMLDivElement>(null) const el = useRef<HTMLDivElement>(null)
const gsapContext = useGsapContext(); const gsapContext = useGsapContext();
useGSAP(() => { useGSAP(() => {
const rect = el.current?.getBoundingClientRect() const rect = el.current?.getBoundingClientRect()
const isInView = rect && rect.top < window.innerHeight const isInView = rect && rect.top < window.innerHeight
const chars = new SplitText(el.current,{type:'chars'}) const chars = new SplitText(el.current, { type: 'chars' })
gsapContext?.addAnimation(gsap.to(el.current,{opacity:100, duration:0}),0) gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100, duration: 0 }), 0)
const fromVars = animation === "slide" const fromVars = animation === "slide"
? {opacity:0, x:-10, duration: 0.2, stagger: {each: 0.08}, ease:'bounce.inOut'} ? { opacity: 0, x: -10, duration: 0.2, stagger: { each: 0.08 }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
: {opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut'} : { opacity: 0, duration: 0.01, stagger: { each: 0.04 }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
if (isInView) { if (isInView) {
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars),position) gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position)
} else { } else {
gsap.from(chars.chars, { ...fromVars, scrollTrigger: { trigger: el.current, start: 'top 85%', scroller: gsapContext?.getScroller() } }) gsap.from(chars.chars, { ...fromVars, scrollTrigger: { trigger: el.current, start: 'top 85%', scroller: gsapContext?.getScroller() } })
} }
}, { dependencies: [] }) }, { dependencies: [] })
return ( return (
<div ref={el} className="opacity-0"> <div ref={el} className={cn(className,"opacity-0")}>
{children} {children}
</div> </div>
) )

View File

@@ -0,0 +1,22 @@
import { type HTMLAttributes, type ReactNode } from "react";
import AnimatedDiv from "./AnimatedDiv";
import { cn } from "~/lib/utils";
const AnimatePopUp = ({
children,
position,
className,
duration=1,
ease='elastic'
}:{
children:ReactNode
position:gsap.Position,
className?:HTMLAttributes<HTMLDivElement>['className']
duration?:number,
ease?:gsap.EaseString|gsap.EaseFunction
}) => {
return (
<AnimatedDiv children={children} position={position} className={cn(className,'h-0 translate-y-[50] overflow-hidden')} height='auto' y={0} overflow='' ease={ease} duration={duration} />
)
}
export default AnimatePopUp;

View File

@@ -0,0 +1,41 @@
import gsap from "gsap";
import { type HTMLAttributes,
type ReactNode, useLayoutEffect, useRef } from "react";
import { useGsapContext } from "~/app/_providers/GsapProvicer";
const AnimatedDiv = (
{
children,
position,
className,
animationMode='to',
...tweenVars
}:
gsap.TweenVars & {
children:ReactNode,
position:gsap.Position,
animationMode?: 'from'|'to',
className?:HTMLAttributes<HTMLDivElement>['className']
}
) => {
const div = useRef<HTMLDivElement>(null);
const gsapContext = useGsapContext()
useLayoutEffect(() => {
let tween:gsap.core.Tween;
switch(animationMode) {
case 'from':
tween = gsap.from(div.current,tweenVars);
break;
case 'to':
tween = gsap.to(div.current,tweenVars);
break;
}
gsapContext?.addAnimation(tween,position)
},[])
return (
<div ref={div} className={className}>
{children}
</div>
)
}
export default AnimatedDiv;

View File

@@ -1,22 +1,21 @@
import { useGSAP } from "@gsap/react"; import { useEffect, useLayoutEffect, useRef } from "react"; import { useGSAP } from "@gsap/react"; import { useEffect, useLayoutEffect, useRef,type ReactNode } from "react";
import { useGsapContext } from "~/app/_providers/GsapProvicer"; import { useGsapContext } from "~/app/_providers/GsapProvicer";
import { SplitText } from "gsap/SplitText"; import { SplitText } from "gsap/SplitText";
import gsap from 'gsap' import gsap from 'gsap'
const AnimatedPageTitle = ( const AnimatedPageTitle = (
{ text, position }: { text: string, position:gsap.Position } { children, position }: { children: ReactNode, position:gsap.Position }
) => { ) => {
const el = useRef<HTMLHeadingElement>(null) const el = useRef<HTMLHeadingElement>(null)
const gsapContext = useGsapContext(); const gsapContext = useGsapContext();
useEffect(() => { useLayoutEffect(() => {
console.log("add animated title with:",position) const split = new SplitText(el.current, { type: "lines,chars", autoSplit:true })
const split = new SplitText(el.current, { type: "chars" })
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100 }),position) gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100 }),position)
gsapContext?.addAnimation(gsap.from(split.chars, { gsapContext?.addAnimation(gsap.from(split.chars, { id: 'titlesplit',
stagger: 0.05, rotate: -90, opacity: 0, x: -10 stagger: 0.05, rotate: -90, opacity: 0, x: -10, onComplete: () => {split.revert()}
}),'>') }),'>')
},[]) },[])
return ( return (
<h1 className="text-4xl opacity-0 font-bold text-balance w-full" ref={el}> {text} </h1> <h1 className="text-4xl break-keep opacity-0 font-bold text-balance w-full" ref={el}> {children} </h1>
) )
} }

View File

@@ -7,7 +7,7 @@ import { ThemeSwitch } from "./ThemeSwitch"
export default function TopNav() { export default function TopNav() {
return ( return (
<div className="fixed lg:w-full right-0 z-50 lg:bg-background"> <div className="fixed backdrop-blur-md lg:w-full right-0 z-50">
<nav className="flex flex-col-reverse lg:flex-row flex-wrap w-20 lg:w-full outline-1 lg:h-10 h-full"> <nav className="flex flex-col-reverse lg:flex-row flex-wrap w-20 lg:w-full outline-1 lg:h-10 h-full">
<div className="flex flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row"> <div className="flex flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row">
<Button className="flex h-10 lg:h-full w-full lg:w-20" asChild variant="outline"> <Button className="flex h-10 lg:h-full w-full lg:w-20" asChild variant="outline">

View File

@@ -2,12 +2,13 @@
import { useGSAP } from '@gsap/react' import { useGSAP } from '@gsap/react'
import gsap from 'gsap' import gsap from 'gsap'
import { SplitText } from 'gsap/SplitText' import { SplitText } from 'gsap/SplitText'
import { ScrollTrigger } from 'gsap/all' import { ScrollTrigger, GSDevTools } from 'gsap/all'
import { createContext, useCallback, useContext, useEffect, useRef, type ReactNode } from 'react' import { createContext, useCallback, useContext, useEffect, useRef, type ReactNode } from 'react'
gsap.registerPlugin(useGSAP) gsap.registerPlugin(useGSAP)
gsap.registerPlugin(ScrollTrigger) gsap.registerPlugin(ScrollTrigger)
gsap.registerPlugin(SplitText) gsap.registerPlugin(SplitText)
gsap.registerPlugin(GSDevTools)
const GsapContext = createContext<{ const GsapContext = createContext<{
addAnimation: ( addAnimation: (
animation: gsap.core.TimelineChild, animation: gsap.core.TimelineChild,
@@ -56,7 +57,10 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
}, []) }, [])
useGSAP(() => { useGSAP(() => {
if (!tl.current) { if (!tl.current) {
tl.current = gsap.timeline({ paused: true }) tl.current = gsap.timeline({ paused: true, id:'mainTimeline', onComplete: ()=>{
console.log('timeline revert')
gsap.getById('mainTimeline')?.revert()
} })
} }
return () => { console.log("gsap cleanup") } return () => { console.log("gsap cleanup") }
}) })

View File

@@ -4,5 +4,6 @@ import { env } from "~/env"
export async function isAdmin() { export async function isAdmin() {
const userid = (await auth()).userId const userid = (await auth()).userId
console.log(userid)
return (userid == env.ADMIN_USER_CLERK_ID) return (userid == env.ADMIN_USER_CLERK_ID)
} }

View File

@@ -2,7 +2,7 @@ import Link from "next/link";
import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger } from "~/components/ui/sidebar"; import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger } from "~/components/ui/sidebar";
import SimpleSidebarGroup from "~/components/ui/simple-sidebar-group"; import SimpleSidebarGroup from "~/components/ui/simple-sidebar-group";
export default async function AdminSideBar() { export default function AdminSideBar() {
return ( return (
<> <>
<SidebarProvider> <SidebarProvider>

View File

@@ -1,8 +1,8 @@
'use server'
import AdminSideBar from "./_components/AdminSideBar"; import AdminSideBar from "./_components/AdminSideBar";
export default async function Admin({children}: Readonly<{children: React.ReactNode}>) { export const dynamic = 'force-dynamic';
export default function Admin({children}: Readonly<{children: React.ReactNode}>) {
return ( return (
<> <>
<AdminSideBar/> <AdminSideBar/>

View File

@@ -31,7 +31,7 @@ export default function CreateUpdateStackForm(params: { className?: string, enti
const deleteMutation = trpc.techStack.delete.useMutation({ onSuccess: makeOnSuccess('delete', form, undefined, path, router) }) const deleteMutation = trpc.techStack.delete.useMutation({ onSuccess: makeOnSuccess('delete', form, undefined, path, router) })
function onSubmit(values: z.infer<typeof schemas.insert>) { function onSubmit(values: z.infer<typeof schemas.insert>) {
setSubmitted(true) setSubmitted(true)
params.entity ? id ?
updateMutation.mutate(values) : updateMutation.mutate(values) :
createMutation.mutate(values); createMutation.mutate(values);
} }

View File

@@ -6,26 +6,31 @@ import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle";
import { Spinner } from "~/components/ui/spinner"; import { Spinner } from "~/components/ui/spinner";
import AnimateTextIn from "../_components/Animated/AnimateIn"; import AnimateTextIn from "../_components/Animated/AnimateIn";
import { ScrollArea } from "~/components/ui/scroll-area"; import { ScrollArea } from "~/components/ui/scroll-area";
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
export default function MusicPage() { export default function MusicPage() {
const { data: tracks, isLoading } = trpc.music.list.useQuery(); const { data: tracks, isLoading } = trpc.music.list.useQuery();
useTimeLine(tracks) useTimeLine(tracks)
return ( return (
<ScrollArea className="w-full h-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4"> <ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
<AnimatedPageTitle position={0} text="Just Some Music I Made" /> <AnimatedPageTitle position={0}><span>Just Some </span> <span>Music I Made</span> </AnimatedPageTitle>
<AnimateTextIn position={0.5}> <div className="flex flex-wrap h-fit content-center">
<div className="flex flex-col lg:flex-row h-fit content-center"> <AnimateTextIn className="flex flex-wrap mr-[1em]" position={0.5}>
<p className="break-after-avoid mr-[1em]">All works on this page are licensed under:</p> <div><p className="break-after-avoid mr-[1em]">All works on this page are licensed under:</p></div>
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> <div><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div>
</AnimateTextIn>
<AnimatePopUp position={2} className="items-center content-center">
<div className="flex flex-row"> <div className="flex flex-row">
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" /> <img className="max-w-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" />
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" /> <img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" />
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" /> <img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" />
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" /> <img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" />
</div> </div>
</AnimatePopUp>
</div> </div>
</AnimateTextIn> <div className="pt-10" />
{tracks && tracks.map((track, i) => ( {tracks && tracks.map((track, i) => (
<Card.AnimatedCard key={track.id} position={i + 1}> <div key={track.id}>
<Card.AnimatedCard position={i + 1}>
<Card.CardHeader> <Card.CardHeader>
<AnimateTextIn position={i + 1.2} animation="slide"> <AnimateTextIn position={i + 1.2} animation="slide">
<Card.CardTitle>{track.title}</Card.CardTitle> <Card.CardTitle>{track.title}</Card.CardTitle>
@@ -35,11 +40,15 @@ export default function MusicPage() {
{track.description && ( {track.description && (
<p className="text-sm text-muted-foreground gsapant">{track.description}</p> <p className="text-sm text-muted-foreground gsapant">{track.description}</p>
)} )}
<AnimatePopUp position={i + 1.3}>
<audio controls className="w-full player" src={track.fileUrl}> <audio controls className="w-full player" src={track.fileUrl}>
Your browser does not support the audio element. Your browser does not support the audio element.
</audio> </audio>
</AnimatePopUp>
</Card.CardContent> </Card.CardContent>
</Card.AnimatedCard> </Card.AnimatedCard>
<div className="pt-5" />
</div>
))} ))}
{!isLoading && !tracks?.length && {!isLoading && !tracks?.length &&
<div className="flex justify-center items-center text-muted-foreground"> <div className="flex justify-center items-center text-muted-foreground">

View File

@@ -5,10 +5,16 @@ import * as Card from "~/components/ui/card";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { StackBadge } from "~/components/StackBadge"; import { StackBadge } from "~/components/StackBadge";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { ScrollArea } from "~/components/ui/scroll-area";
import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle";
import AnimateTextIn from "../_components/Animated/AnimateIn";
import { useTimeLine } from "../_providers/GsapProvicer";
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
import { Button } from "~/components/ui/button";
export default function ProjectsPage() { export default function ProjectsPage() {
const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery(); const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery();
useTimeLine(projects)
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex justify-center items-center min-h-[200px] text-muted-foreground"> <div className="flex justify-center items-center min-h-[200px] text-muted-foreground">
@@ -26,17 +32,22 @@ export default function ProjectsPage() {
} }
return ( return (
<div className="w-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4"> <ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
{projects.map((project) => ( <AnimatedPageTitle position={0}><span>Project I've Been</span><span> Working on</span> </AnimatedPageTitle>
<Card.Card key={project.id}> <div className="pt-10" />
{projects.map((project, i) => (
<div key={i}>
<Card.AnimatedCard position={i + 1.2} key={project.id}>
<Card.CardHeader> <Card.CardHeader>
<div className="flex items-start justify-between gap-2 flex-wrap"> <div className="flex items-start justify-between gap-2 flex-wrap">
<Card.CardTitle>{project.title}</Card.CardTitle> <AnimateTextIn position={i + 1.4} animation="slide"><Card.CardTitle>{project.title}</Card.CardTitle></AnimateTextIn>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{project.sourceType && ( {project.sourceType && (
<AnimatePopUp position={i + 2} duration={2}>
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}> <Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
{project.sourceType === "open" ? "Open Source" : "Closed Source"} {project.sourceType === "open" ? "Open Source" : "Closed Source"}
</Badge> </Badge>
</AnimatePopUp>
)} )}
{project.releaseStatus && ( {project.releaseStatus && (
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}> <Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
@@ -50,44 +61,54 @@ export default function ProjectsPage() {
<Card.CardContent className="flex flex-col gap-3"> <Card.CardContent className="flex flex-col gap-3">
{project.description && ( {project.description && (
<div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground"> <div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground">
<Markdown>{project.description}</Markdown> <AnimatePopUp position={i + 1.4} duration={project.description.length / 20}>
<AnimateTextIn position={i + 1.5} animation="slide"><Markdown>{project.description}</Markdown></AnimateTextIn></AnimatePopUp>
</div>
)}
<div className="flex flex-row">
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{project.techStack.stackItems.map((item, k) => (
<AnimatePopUp key={k} position={(i + 2) + k * 0.5}> <StackBadge key={item} item={item} /> </AnimatePopUp>
))}
</div> </div>
)} )}
{(project.sourceLink || project.releaseLink) && ( {(project.sourceLink || project.releaseLink) && (
<div className="flex gap-3 flex-wrap"> <div className="ml-auto flex-col lg:flex-row justify-center gap-5">
{project.sourceLink && ( {project.sourceLink &&
<Button variant='outline' className="cursor-pointer mb-3 lg:mb-0 lg:mr-3 min-w-18">
<a <a
href={project.sourceLink} href={project.sourceLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors" className='items-center'
> >
Source Source
</a> </a>
)} </Button>
{project.releaseLink && ( }
{project.releaseLink &&
<Button variant='default' className="cursor-pointer min-w-18 items-center">
<a <a
href={project.releaseLink} href={project.releaseLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors" className='items-center'
> >
Live Live
</a> </a>
)} </Button>
}
</div> </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> </div>
)}
</Card.CardContent> </Card.CardContent>
)} )}
</Card.Card> </Card.AnimatedCard>
))} <div className="pt-5" />
</div> </div>
))}
</ScrollArea>
); );
} }