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

Dashboard #15

Closed
wants to merge 14 commits into from
Closed
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ next-env.d.ts
# expo
.expo/
dist/
expo-env.d.ts
apps/expo/.gitignore

# production
Expand Down
4 changes: 1 addition & 3 deletions apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
},
"dependencies": {
"@acme/api": "workspace:^",
"@clerk/clerk-react": "^4.30.5",
"@clerk/clerk-sdk-node": "^4.13.9",
"@clerk/nextjs": "^4.29.7",
"@dnd-kit/core": "^6.1.0",
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
Expand Down
28 changes: 28 additions & 0 deletions apps/nextjs/src/app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

import { createClient } from "@/lib/utils/supabase/server";

export async function signup(formData: FormData) {
const supabase = createClient();

// type-casting here for convenience
// in practice, you should validate your inputs
const data = {
email: formData.get("email") as string,
password: formData.get("password") as string,
};

const { error } = await supabase.auth.signUp(data);
console.log("error", error);
if (error) {
// TODO: Form check error
redirect("/error");
}

revalidatePath("/", "layout");
redirect("/");
}

5 changes: 1 addition & 4 deletions apps/nextjs/src/app/auth/confirm/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { type EmailOtpType } from "@supabase/supabase-js";
import { cookies } from "next/headers";
import { type NextRequest, NextResponse } from "next/server";

import { createClient } from "@/lib/utils/supabase/server";

export async function GET(request: NextRequest) {
const cookieStore = cookies();

const { searchParams } = new URL(request.url);
const token_hash = searchParams.get("token_hash");
const type = searchParams.get("type") as EmailOtpType | null;
Expand All @@ -18,7 +15,7 @@ export async function GET(request: NextRequest) {
redirectTo.searchParams.delete("type");

if (token_hash && type) {
const supabase = createClient(cookieStore);
const supabase = createClient();

const { error } = await supabase.auth.verifyOtp({
type,
Expand Down
159 changes: 159 additions & 0 deletions apps/nextjs/src/app/dashboard/Assignments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"use client";

import { useState } from "react";
import { Metadata } from "next";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { DndContext, DragOverlay, useDroppable } from "@dnd-kit/core";

import { AssignmentCard, MemberCard } from "./components/cards";
import { CalendarDateRangePicker } from "./components/date-range-picker";
import { MainNav } from "./components/main-nav";
import { Overview } from "./components/overview";
import { RecentSales } from "./components/recent-sales";
import { Search } from "./components/search";
import TeamSwitcher from "./components/team-switcher";
import { UserNav } from "./components/user-nav";

const tags = Array.from({ length: 50 }).map(
(_, i, a) => `v1.2.0-beta.${a.length - i}`,
);

export default function Assignments() {
// XXX: Use real data via tRPC
const [members, setMembers] = useState<{ [key: string]: string[] }>(
Object.fromEntries(tags.map((x) => [`${x}M`, []])),
);
const [assignments, setAssignments] = useState(tags.map((x) => `${x}A`));
const [activeId, setActiveId] = useState<string | null>(null);
// Inverted members
const reverseAssignmentToMembers = Object.fromEntries(
Object.entries(members).flatMap(([k, v]) => v.map((x) => [x, k])),
);
// const [parent, setParent] = useState(null);
return (
<DndContext
onDragStart={function handleDragStart(event) {
const active = event.active as { id: string };
console.log("start", active);
setActiveId(active.id);
}}
onDragEnd={function handleDragEnd(event) {
const overId = event.over?.id;
console.log("end", overId, activeId, members, assignments);
if (overId === undefined) {
// Drag action was cancelled
return;
}
if (overId === "ASSIGNMENT_LIST") {
if (assignments.includes(activeId as string)) {
// Assignment already exists in the list
return;
}
setAssignments((assignments) => [...assignments, activeId as string]);
setMembers((members) => {
const prevMember = reverseAssignmentToMembers[activeId as string];
return {
...members,
[prevMember]: members[prevMember].filter((x) => x !== activeId),
};
});
setActiveId(null);
return;
}
if (members[overId].includes(activeId as string)) {
// Assignment already exists in the member's list
return;
}
const prevMember = reverseAssignmentToMembers[activeId as string];
if (prevMember !== undefined) {
setMembers((members) => {
return {
...members,
[overId]: [...members[overId], activeId as string],
[prevMember]: members[prevMember].filter((x) => x !== activeId),
};
});
} else {
setMembers((members) => {
return {
...members,
[overId]: [...members[overId], activeId as string],
};
});
}
setAssignments((assignments) =>
assignments.filter((x) => x !== activeId),
);
setActiveId(null);
}}
>
<ResizablePanelGroup
direction="horizontal"
className="max-w-screen h-rounded-lg h-full border"
>
<ResizablePanel defaultSize={50}>
<ScrollArea className="h-full w-full rounded-md border">
<div className="flex flex-wrap">
{Object.entries(members).map(([name, values]) => (
<MemberCard key={name} user={name} assignments={values} />
))}
</div>
</ScrollArea>
</ResizablePanel>
<ResizableHandle withHandle={true} />
<ResizablePanel defaultSize={50}>
<AssignmentList assignments={assignments} />
<DragOverlay>
{activeId && <AssignmentCard assignment={activeId} />}
</DragOverlay>
{/* <ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={25}>
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Two</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={75}>
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Three</span>
</div>
</ResizablePanel>
</ResizablePanelGroup> */}
</ResizablePanel>
</ResizablePanelGroup>
</DndContext>
);
}
function AssignmentList({ assignments }: { assignments: string[] }) {
const { isOver, setNodeRef } = useDroppable({
id: "ASSIGNMENT_LIST",
});
return (
<ScrollArea
ref={setNodeRef}
className={`h-full w-full rounded-md border ${isOver && "bg-accent"}`}
>
<div className="flex flex-wrap">
{assignments.map((x) => (
<AssignmentCard key={x} assignment={x} />
))}
</div>
</ScrollArea>
);
}
160 changes: 160 additions & 0 deletions apps/nextjs/src/app/dashboard/components/cards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import * as React from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { DragOverlay, useDraggable, useDroppable } from "@dnd-kit/core";

// XXX: Properly conform to database types
export function MemberCard({
user,
assignments,
}: {
user: string;
assignments: string[];
}) {
const { isOver, setNodeRef } = useDroppable({
id: user,
});
return (
// <DragOverlay>
<Card
className={`h-fit w-fit transition-all ${isOver && "bg-accent"}`}
ref={setNodeRef}
>
<CardHeader>
<CardTitle>
<Avatar className="h-8 w-8">
<AvatarImage src="/avatars/01.png" alt="@shadcn" />
<AvatarFallback>SC</AvatarFallback>
</Avatar>
</CardTitle>
<CardDescription>Bryan Hu ({user})</CardDescription>
</CardHeader>
<CardContent className="h-full">
{assignments.length === 0 ? (
<p className="text-muted-foreground max-w-sm text-sm">
No assignments were assigned
</p>
) : (
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>
See ({assignments.length}) assignment
{assignments.length > 1 && "s"}
</AccordionTrigger>
<AccordionContent>
{assignments.map((assignment) => (
<AssignmentCard key={assignment} assignment={assignment} />
))}
</AccordionContent>
</AccordionItem>
</Accordion>
)}

{/* </Button> */}
{/* <form>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Name of your project" />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="framework">Framework</Label>
<Select>
<SelectTrigger id="framework">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="next">Next.js</SelectItem>
<SelectItem value="sveltekit">SvelteKit</SelectItem>
<SelectItem value="astro">Astro</SelectItem>
<SelectItem value="nuxt">Nuxt.js</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</form> */}
</CardContent>
{/* <CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter> */}
</Card>
);
}
export function AssignmentCard({ assignment }: { assignment: string }) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: assignment,
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;

return (
<Card
className="z-50 h-fit w-fit"
ref={setNodeRef}
style={style}
{...listeners}
{...attributes}
>
<CardHeader>
<CardTitle>Match Scouting Assignment</CardTitle>
<CardDescription>{assignment}</CardDescription>
</CardHeader>
<CardContent>
{" "}
I AM BORING
{/* </Button> */}
{/* <form>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Name of your project" />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="framework">Framework</Label>
<Select>
<SelectTrigger id="framework">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="next">Next.js</SelectItem>
<SelectItem value="sveltekit">SvelteKit</SelectItem>
<SelectItem value="astro">Astro</SelectItem>
<SelectItem value="nuxt">Nuxt.js</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</form> */}
</CardContent>
{/* <CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter> */}
</Card>
);
}
Loading
Loading