Skip to content

Commit

Permalink
Merge pull request #64 from hubcio2115/project-dashboard-view
Browse files Browse the repository at this point in the history
  • Loading branch information
hubcio2115 committed Sep 28, 2024
2 parents c670a1e + e636ddb commit f9bc3d9
Show file tree
Hide file tree
Showing 21 changed files with 740 additions and 140 deletions.
8 changes: 7 additions & 1 deletion apps/web/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ const config = {
protocol: "https",
hostname: "yt3.ggpht.com",
port: "",
pathname: "/ytc/**",
pathname: "/**",
},
{
protocol: "https",
hostname: "i.ytimg.com",
port: "",
pathname: "/vi/**",
},
],
},
Expand Down
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@t3-oss/env-nextjs": "^0.10.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.51.23",
"@tanstack/react-query-devtools": "^5.51.23",
"@total-typescript/ts-reset": "^0.5.1",
"@uidotdev/usehooks": "^2.4.1",
"@vercel/postgres": "^0.9.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand Down
22 changes: 19 additions & 3 deletions apps/web/src/app/api/organizations/[name]/projects/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "~/server/actions/organization";

export async function GET(
_req: NextRequest,
req: NextRequest,
{ params }: { params: { name: string } },
) {
const [organization, err] = await getOwnOrganizationByName(params.name);
Expand All @@ -22,7 +22,23 @@ export async function GET(
);
}

const projects = await getOrganizationProjects(organization.id);
const page = req.nextUrl.searchParams.get("page");
if (page === null) {
return NextResponse.json(
{ message: "Page query param is required." },
{ status: 400 },
);
}

const query = req.nextUrl.searchParams.get("q");
if (query === null) {
return NextResponse.json(
{ message: "Q query param is required." },
{ status: 400 },
);
}

const projectsResponse = await getOrganizationProjects(organization.id, +page, query);

return NextResponse.json(projects);
return NextResponse.json(projectsResponse);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from "next/server";
import { isUserInOrganization } from "~/server/api/utils/organizations";
import { getVideo } from "~/server/api/utils/project";
import { auth } from "~/server/auth";

export async function GET(
req: NextRequest,
{ params }: { params: { id: string; name: string } },
) {
const session = await auth();

const isUserAuthorized =
session?.user && (await isUserInOrganization(session.user.id, params.name));

if (!isUserAuthorized) {
return NextResponse.json({ message: "UNAUTHORIZED" }, { status: 401 });
}

const [video, err] = await getVideo(params.name, params.id);

if (err !== null) {
return NextResponse.json(
{ message: "Something went wrong on our end", cause: err },
{ status: 500 },
);
}

return NextResponse.json(video);
}
25 changes: 25 additions & 0 deletions apps/web/src/app/api/youtube/channel/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
import { getChannel } from "~/server/api/utils/project";
import { auth } from "~/server/auth";

export async function GET(
req: NextRequest,
{ params }: { params: { id: string } },
) {
const session = auth();

if (!session) {
return NextResponse.json({ message: "UNAUTHORIZED" }, { status: 401 });
}

const [categories, err] = await getChannel(params.id);

if (err !== null) {
return NextResponse.json(
{ message: "Something went wrong on our end", cause: err },
{ status: 500 },
);
}

return NextResponse.json(categories);
}
14 changes: 10 additions & 4 deletions apps/web/src/app/dashboard/[name]/overview/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { redirect } from "next/navigation";
import { Suspense } from "react";
import ProjectGrid from "~/components/dashboard/project-grid";
import ProjectsSkeleton from "~/components/dashboard/project-grid-skeleton";
import ProjectPagination from "~/components/dashboard/project-pagination";
import SearchNavigation from "~/components/dashboard/search-navigation";

import { getOwnOrganizations } from "~/server/actions/organization";
Expand All @@ -23,12 +25,16 @@ export default async function DashboardOverviewPage({
const organization = organizations.find((org) => name === org.name);

return organization ? (
<div className="mx-auto flex flex-col flex-1 items-center md:px-2">
<div className="mx-auto flex flex-col flex-1 items-center md:px-2 container pb-10 gap-8">
<SearchNavigation orgName={name} />

<Suspense fallback={<div>Loading...</div>}>
<ProjectGrid organization={organization} />
</Suspense>
<ProjectPagination organizationName={name} />

<div className="grid grid-cols-1 gap-5 lg:grid-cols-2 lg:grid-rows-6 2xl:w-[1300px] 2xl:grid-cols-3 2xl:grid-rows-4 flex-1">
<Suspense fallback={<ProjectsSkeleton />}>
<ProjectGrid organization={organization} />
</Suspense>
</div>
</div>
) : (
redirect("/404")
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/create-project/project-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const defaultValues: InsertProject = {
defaultLanguage: "none",
tags: "",
embeddable: true,
privacyStatus: "private",
privacyStatus: "unlisted",
publicStatsViewable: true,
selfDeclaredMadeForKids: false,
notifySubscribers: true,
Expand Down
58 changes: 42 additions & 16 deletions apps/web/src/components/dashboard/project-card.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
"use client";

import Image from "next/image";
import Link from "next/link";
import type { Project } from "~/lib/validators/project";
import { useVideoSuspenseQuery } from "~/lib/queries/useVideoQuery";
import { useParams } from "next/navigation";
import { useChannelSuspenseQuery } from "~/lib/queries/useChannelQuery";

interface ProjectCardProps {
project: Project;
}

export default function VideoCard({ project }: ProjectCardProps) {
const { name } = useParams() as { name: string };

const { data: video } = useVideoSuspenseQuery(project.videoId!, name);
const { data: channel } = useChannelSuspenseQuery(project.channelId);

return (
<Card className="max-w-[460px] hover:cursor-pointer hover:bg-slate-50">
<CardHeader>
<CardTitle className="h-6 text-base sm:text-2xl">
{project.title}
</CardTitle>
<div className="max-w-[350px] sm:max-w-96 mx-auto">
<Link
href={`/dashboard/${name}/project/${project.id}`}
className="flex items-center max-h-min flex-col gap-2"
>
<div className="h-60 w-[320px] sm:w-96 rounded-lg overflow-hidden">
<Image
className="h-60 w-[320px] sm:w-96"
alt="project thumbnail"
src={video.snippet?.thumbnails?.standard?.url ?? ""}
width={video.snippet?.thumbnails?.standard?.width ?? 320}
height={video.snippet?.thumbnails?.standard?.height ?? 180}
/>
</div>

<div className="flex w-full gap-2">
<Image
className="w-10 h-10 rounded-full"
alt="channel thumbnail"
src={channel.snippet?.thumbnails?.default?.url ?? ""}
width={40}
height={40}
/>

<CardDescription className="h-10 overflow-hidden">
{project.description}
</CardDescription>
</CardHeader>
</Card>
<div className="flex flex-col gap-1 w-full">
<p className="font-bold">{project.title}</p>
<p className="text-sm text-slate-500">{channel.snippet?.title}</p>
</div>
</div>
</Link>
</div>
);
}
33 changes: 33 additions & 0 deletions apps/web/src/components/dashboard/project-grid-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Skeleton } from "../ui/skeleton";

export default function ProjectsSkeleton() {
return (
<>
{(() => {
const projects = [];
for (let i = 0; i < 12; i++) {
projects.push(
<div
key={i}
className="flex items-center flex-col max-w-[350px] sm:max-w-96 mx-auto gap-2"
>
<Skeleton className="h-60 w-[320px] sm:w-96 rounded-lg" />

<div className="w-full flex gap-2">
<Skeleton className="min-w-10 w-10 h-10 rounded-full" />

<div className="flex flex-col gap-1 w-full">
<Skeleton className="w-full h-5" />
<Skeleton className="w-64 h-5" />
<Skeleton className="w-32 h-5" />
</div>
</div>
</div>,
);
}

return projects;
})()}
</>
);
}
75 changes: 35 additions & 40 deletions apps/web/src/components/dashboard/project-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
"use client";

import Link from "next/link";
import ProjectSmallCard from "./project-small-card";
import ProjectCard from "./project-card";
import type { Organization } from "~/lib/validators/organization";
import { useSuspenseQuery } from "@tanstack/react-query";
import { z } from "zod";
import { projectSchema } from "~/lib/validators/project";
import { env } from "~/env";
import Image from "next/image";
import { useSearchParams } from "next/navigation";
import ky from "ky";
import { usePathname, useSearchParams, useRouter } from "next/navigation";
import { useProjectsPaginatedQuery } from "~/lib/queries/useProjectsQuery";
import ProjectsSkeleton from "./project-grid-skeleton";

function EmptyProjectsInfo() {
return (
<div className="my-auto text-center">
<div className="my-auto text-center col-span-full row-span-full">
<Image
src="/img/suprised_pikachu.png"
alt="suprised pikachu"
Expand All @@ -41,42 +37,41 @@ interface ProjectGridProps {
}

export default function ProjectGrid({ organization }: ProjectGridProps) {
const { data: projects } = useSuspenseQuery({
queryKey: ["projects"],
queryFn: async () => {
const res = await ky
.get(
`${env.NEXT_PUBLIC_API_URL}/api/organizations/${organization.name}/projects`,
)
.json();

const data = z.array(projectSchema).safeParse(res);
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

if (data.error) {
throw data.error;
}
const page = searchParams.get("page");
if (page === null || isNaN(parseInt(page))) {
const params = new URLSearchParams(searchParams);
params.set("page", "1");

return data.data;
},
});
router.push(pathname + "?" + params.toString());
}

const searchParams = useSearchParams();
const query = searchParams.get("q");
if (query === null) {
const params = new URLSearchParams(searchParams);
params.set("q", "");

const listDisplayType = searchParams.get("listType");
router.push(pathname + "?" + params.toString());
}

return projects.length === 0 ? (
<EmptyProjectsInfo />
) : listDisplayType === null || searchParams.get("listType") === "list" ? (
<div className="my-5 flex flex-col justify-center gap-4">
{projects.map((project) => (
<ProjectSmallCard key={project.id} project={project} />
))}
</div>
) : (
<div className="my-5 grid grid-cols-1 gap-5 lg:grid-cols-2 2xl:w-[1300px] 2xl:grid-cols-3">
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
const { data } = useProjectsPaginatedQuery(
organization.name,
page ? +page : 0,
query ?? "",
);

if (!data?.projects) {
return <ProjectsSkeleton />;
}

if (data.projects?.length === 0) {
return <EmptyProjectsInfo />;
}

return data.projects.map((project) => (
<ProjectCard key={project.id} project={project} />
));
}
Loading

0 comments on commit f9bc3d9

Please sign in to comment.