finish up project page
This commit is contained in:
5
bun.lock
5
bun.lock
@@ -79,6 +79,7 @@
|
|||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"rehype-highlight": "^7.0.2",
|
"rehype-highlight": "^7.0.2",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"shadcn": "^4.0.2",
|
"shadcn": "^4.0.2",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -550,10 +551,10 @@
|
|||||||
|
|
||||||
"@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
|
"@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
|
||||||
|
|
||||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
|
||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
|
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||||
|
|
||||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
"@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
|
"@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"rehype-highlight": "^7.0.2",
|
"rehype-highlight": "^7.0.2",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"shadcn": "^4.0.2",
|
"shadcn": "^4.0.2",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
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 function AdminSideBar() {
|
export default function AdminSideBar() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SidebarProvider>
|
<Sidebar variant="floating" className="h-[96%] mt-10 z-[51]">
|
||||||
<Sidebar className="z-[51]">
|
|
||||||
<SidebarTrigger className="absolute z-[52] left-65 top-100" />
|
<SidebarTrigger className="absolute z-[52] left-65 top-100" />
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
|
<ScrollArea>
|
||||||
<SimpleSidebarGroup lable="CV">
|
<SimpleSidebarGroup lable="CV">
|
||||||
<Link href={"/admin/cv/category/create"}> Create Category </Link>
|
<Link href={"/admin/cv/category/create"}> Create Category </Link>
|
||||||
<Link href={"/admin/cv/entry/create"}> Create Entry </Link>
|
<Link href={"/admin/cv/entry/create"}> Create Entry </Link>
|
||||||
@@ -29,9 +30,9 @@ export default function AdminSideBar() {
|
|||||||
<SimpleSidebarGroup lable="Chat">
|
<SimpleSidebarGroup lable="Chat">
|
||||||
<Link href={"/admin/chat"}> System Prompt </Link>
|
<Link href={"/admin/chat"}> System Prompt </Link>
|
||||||
</SimpleSidebarGroup>
|
</SimpleSidebarGroup>
|
||||||
|
</ScrollArea>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</SidebarProvider>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,8 @@ import CreateUpdateCvCategoryForm from "../_components/CreateUpdateForm";
|
|||||||
export default function CvPage() {
|
export default function CvPage() {
|
||||||
const categories = trpc.category.select.useQuery({}, { refetchInterval: 1000 });
|
const categories = trpc.category.select.useQuery({}, { refetchInterval: 1000 });
|
||||||
const entires = trpc.entry.select.useSuspenseQuery({}, { refetchInterval: 1000 })
|
const entires = trpc.entry.select.useSuspenseQuery({}, { refetchInterval: 1000 })
|
||||||
const gsap = useGsapContext()
|
|
||||||
const container = useRef<HTMLDivElement>(null);
|
|
||||||
useGSAP(() => {
|
|
||||||
gsap?.from('.gsapan', { x: -100, opacity: 0, duration: 0.5, stagger: { each: 0.3 } });
|
|
||||||
}, { scope: container, dependencies: [categories.status], revertOnUpdate: true });
|
|
||||||
return (
|
return (
|
||||||
<div ref={container} className="w-5/6 lg:w-1/2 flex flex-col gap-3">
|
<>
|
||||||
{categories.data == undefined ?
|
{categories.data == undefined ?
|
||||||
<div className="gsapan"></div>
|
<div className="gsapan"></div>
|
||||||
:
|
:
|
||||||
@@ -64,6 +59,6 @@ export default function CvPage() {
|
|||||||
<CollapsibleForm entityName="Category" form={CreateUpdateCvCategoryForm} />
|
<CollapsibleForm entityName="Category" form={CreateUpdateCvCategoryForm} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,9 @@ import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
|||||||
|
|
||||||
export default function CvPage() {
|
export default function CvPage() {
|
||||||
const entires = trpc.entry.select.useQuery({});
|
const entires = trpc.entry.select.useQuery({});
|
||||||
const gsap = useGsapContext()
|
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
useGSAP(() => {
|
|
||||||
gsap?.from('.gsapan', { x: -100, opacity: 0, duration: 0.5, stagger: { each: 0.3 } })
|
|
||||||
}, { scope: container, dependencies: [entires.status], revertOnUpdate: true });
|
|
||||||
return (
|
return (
|
||||||
<div ref={container} className="w-5/6 lg:w-1/2 flex flex-col gap-3">
|
<>
|
||||||
{entires.data == undefined ?
|
{entires.data == undefined ?
|
||||||
<div className="gsapan"></div>
|
<div className="gsapan"></div>
|
||||||
:
|
:
|
||||||
@@ -40,6 +36,6 @@ export default function CvPage() {
|
|||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
|
import { SidebarProvider } from "~/components/ui/sidebar";
|
||||||
import AdminSideBar from "./_components/AdminSideBar";
|
import AdminSideBar from "./_components/AdminSideBar";
|
||||||
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default function Admin({children}: Readonly<{children: React.ReactNode}>) {
|
export default function Admin({children}: Readonly<{children: React.ReactNode}>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<SidebarProvider>
|
||||||
<AdminSideBar/>
|
<AdminSideBar/>
|
||||||
<main className="absolute flex items-center content-center justify-center flex-wrap w-[100vw] left-0 top-15">
|
<ScrollArea className="px-10 lg:px-0 w-full h-screen pb-10 max-w-4xl mx-auto pt-10">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</ScrollArea>
|
||||||
|
</SidebarProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function ProjectList() {
|
|||||||
const projects = trpc.project.select.useQuery({}, { refetchInterval: 1000 })
|
const projects = trpc.project.select.useQuery({}, { refetchInterval: 1000 })
|
||||||
const techStacks = trpc.techStack.select.useSuspenseQuery({}, { refetchInterval: 1000 })
|
const techStacks = trpc.techStack.select.useSuspenseQuery({}, { refetchInterval: 1000 })
|
||||||
return (
|
return (
|
||||||
<div className="w-5/6 lg:w-1/2 flex flex-col gap-3">
|
<>
|
||||||
{
|
{
|
||||||
projects.data == undefined ?
|
projects.data == undefined ?
|
||||||
<></> :
|
<></> :
|
||||||
@@ -55,6 +55,6 @@ export default function ProjectList() {
|
|||||||
<CollapsibleForm entityName="Project" form={CreateUpdateProjectForm} />
|
<CollapsibleForm entityName="Project" form={CreateUpdateProjectForm} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import AnimateTextIn from "../_components/Animated/AnimateIn";
|
|||||||
import { useTimeLine } from "../_providers/GsapProvicer";
|
import { useTimeLine } from "../_providers/GsapProvicer";
|
||||||
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
|
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import remarkGfm from "remark-gfm"
|
||||||
|
|
||||||
export default function ProjectsPage() {
|
export default function ProjectsPage() {
|
||||||
const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery();
|
const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery();
|
||||||
@@ -61,8 +62,9 @@ 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">
|
||||||
<AnimatePopUp position={i + 1.4} duration={project.description.length / 20}>
|
<AnimatePopUp position={i + 1.4} duration={10}>
|
||||||
<AnimateTextIn position={i + 1.5} animation="slide"><Markdown>{project.description}</Markdown></AnimateTextIn></AnimatePopUp>
|
<Markdown remarkPlugins={[remarkGfm]}>{project.description}</Markdown>
|
||||||
|
</AnimatePopUp>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
|
|||||||
@@ -1,12 +1,92 @@
|
|||||||
import { publicProcedure, router } from "~/server/trpc";
|
import { publicProcedure, router } from "~/server/trpc";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
|
|
||||||
|
type ReadmeRequest = {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getReadmeRequest(sourceLink: string): ReadmeRequest | null {
|
||||||
|
let url: URL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
url = new URL(sourceLink);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathParts = url.pathname.split("/").filter(Boolean);
|
||||||
|
const [owner, repo] = pathParts;
|
||||||
|
|
||||||
|
if (!owner || !repo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoName = repo.replace(/\.git$/, "");
|
||||||
|
|
||||||
|
if (url.hostname === "github.com" || url.hostname === "www.github.com") {
|
||||||
|
return {
|
||||||
|
url: `https://raw.githubusercontent.com/${owner}/${repoName}/main/README.md`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.hostname.includes("gitea.")) {
|
||||||
|
return {
|
||||||
|
url: `${url.origin}/${owner}/${repoName}/raw/branch/main/README.md`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchReadme(sourceLink: string) {
|
||||||
|
const readmeRequest = getReadmeRequest(sourceLink);
|
||||||
|
|
||||||
|
if (!readmeRequest) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(readmeRequest.url, {
|
||||||
|
headers: {
|
||||||
|
Accept: "text/plain",
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.text();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const projectRouter = router({
|
export const projectRouter = router({
|
||||||
listWithStack: publicProcedure.query(async () => {
|
listWithStack: publicProcedure.query(async () => {
|
||||||
return db.query.project.findMany({
|
const projects = await db.query.project.findMany({
|
||||||
with: {
|
with: {
|
||||||
techStack: true,
|
techStack: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
projects.map(async (project) => {
|
||||||
|
if (project.description?.length !== 0 || !project.sourceLink) {
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
description: await fetchReadme(project.sourceLink),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user