Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project-dashboard-view #64

Merged
merged 21 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading