mirror of
https://github.com/SrIzan10/echospace.git
synced 2026-06-06 00:56:54 +00:00
feat: initial collaboration implementation
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "1.0.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.4.1",
|
||||
"lucia": "^3.1.1",
|
||||
"lucide-react": "^0.368.0",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
-- thanks ai
|
||||
|
||||
-- Step 1: Create the new UserProject table
|
||||
CREATE TABLE "UserProject" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "UserProject_pkey" PRIMARY KEY ("userId","projectId")
|
||||
);
|
||||
|
||||
-- Step 2: Insert existing userId and projectId pairs into the UserProject table
|
||||
INSERT INTO "UserProject" ("userId", "projectId")
|
||||
SELECT "userId", "id" FROM "Project" WHERE "userId" IS NOT NULL;
|
||||
|
||||
-- Step 3: Drop the foreign key constraint and the userId column from the Project table
|
||||
ALTER TABLE "Project" DROP CONSTRAINT "Project_userId_fkey";
|
||||
ALTER TABLE "Project" DROP COLUMN "userId";
|
||||
|
||||
-- Step 4: Add foreign key constraints to the UserProject table
|
||||
ALTER TABLE "UserProject" ADD CONSTRAINT "UserProject_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "UserProject" ADD CONSTRAINT "UserProject_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- Step 5: Drop the unnecessary _UserProjects table and its constraints
|
||||
DROP TABLE IF EXISTS "_UserProjects";
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[inviteCode]` on the table `Project` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ADD COLUMN "inviteCode" TEXT NOT NULL DEFAULT floor(random() * 90000000 + 10000000)::text;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_UserProjects" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_UserProjects_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_UserProjects_B_index" ON "_UserProjects"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Feedback_projectId_idx" ON "Feedback"("projectId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Project_inviteCode_key" ON "Project"("inviteCode");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UserProject_userId_idx" ON "UserProject"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UserProject_projectId_idx" ON "UserProject"("projectId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_UserProjects" ADD CONSTRAINT "_UserProjects_A_fkey" FOREIGN KEY ("A") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_UserProjects" ADD CONSTRAINT "_UserProjects_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ALTER COLUMN "inviteCode" SET DEFAULT floor(random() * 90000000 + 10000000)::text;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "UserProject" ADD COLUMN "isOwner" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -14,12 +14,13 @@ datasource db {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
githubId String @unique
|
||||
id String @id @default(cuid())
|
||||
githubId String @unique
|
||||
username String
|
||||
installations String[]
|
||||
projects Project[]
|
||||
projects Project[] @relation("UserProjects")
|
||||
sessions Session[]
|
||||
UserProject UserProject[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
@@ -27,6 +28,8 @@ model Session {
|
||||
userId String
|
||||
expiresAt DateTime
|
||||
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Project {
|
||||
@@ -37,17 +40,32 @@ model Project {
|
||||
customData String[]
|
||||
rateLimitReq Int @default(5)
|
||||
rateLimitTime Int @default(60)
|
||||
// 8 digit random number
|
||||
inviteCode String @unique @default(dbgenerated("floor(random() * 90000000 + 10000000)::text"))
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
feedback Feedback[]
|
||||
users User[] @relation("UserProjects")
|
||||
feedback Feedback[]
|
||||
UserProject UserProject[]
|
||||
}
|
||||
|
||||
model UserProject {
|
||||
userId String
|
||||
projectId String
|
||||
isOwner Boolean @default(false)
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
|
||||
@@id([userId, projectId])
|
||||
@@index([userId])
|
||||
@@index([projectId])
|
||||
}
|
||||
|
||||
model Feedback {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
message String
|
||||
customData String
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
@@index([projectId])
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@ export default async function Page() {
|
||||
const { user } = await validateRequest();
|
||||
const db = await prisma.project.findMany({
|
||||
where: {
|
||||
userId: user!.id,
|
||||
UserProject: {
|
||||
some: {
|
||||
userId: user!.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (db.length === 0) {
|
||||
@@ -16,9 +20,16 @@ export default async function Page() {
|
||||
<div className="text-center flex flex-col gap-4 pt-4">
|
||||
<h2>No projects found</h2>
|
||||
<p className="text-muted-foreground">Create a project to get started</p>
|
||||
<Link href="/create">
|
||||
<Button size={'sm'}>Create Project</Button>
|
||||
</Link>
|
||||
<div className="flex justify-center gap-5">
|
||||
<Link href="/create">
|
||||
<Button size={'sm'}>Create</Button>
|
||||
</Link>
|
||||
<Link href="/join">
|
||||
<Button size={'sm'} variant={'secondary'}>
|
||||
Join
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
36
src/app/(protected)/join/action.ts
Normal file
36
src/app/(protected)/join/action.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
'use server';
|
||||
|
||||
import { validateRequest } from "@/lib/auth";
|
||||
import prisma from "@/lib/db";
|
||||
|
||||
export async function join(code: string) {
|
||||
const { user } = await validateRequest();
|
||||
if (!user) {
|
||||
return { success: false, message: "Unauthorized" };
|
||||
}
|
||||
const numCode = parseInt(code);
|
||||
if (isNaN(numCode) || code.length !== 8) {
|
||||
return { success: false, message: "Invalid code" };
|
||||
}
|
||||
|
||||
const project = await prisma.project.findFirst({
|
||||
where: { inviteCode: code },
|
||||
include: {
|
||||
UserProject: {
|
||||
where: { userId: user.id },
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!project) {
|
||||
return { success: false, message: "Project not found" };
|
||||
}
|
||||
if (project.UserProject.length > 0) {
|
||||
return { success: false, message: "Already joined project" };
|
||||
}
|
||||
|
||||
await prisma.project.update({
|
||||
where: { id: project.id },
|
||||
data: { UserProject: { create: { userId: user.id } } },
|
||||
});
|
||||
return { success: true, message: "Joined project", id: project.id };
|
||||
}
|
||||
61
src/app/(protected)/join/page.tsx
Normal file
61
src/app/(protected)/join/page.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp';
|
||||
import { REGEXP_ONLY_DIGITS } from 'input-otp';
|
||||
import React from 'react';
|
||||
import { join } from './action';
|
||||
import { toast } from 'sonner';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const [value, setValue] = React.useState('');
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center pt-10 gap-5">
|
||||
<h2>Join another user's project!</h2>
|
||||
<InputOTP
|
||||
maxLength={8}
|
||||
pattern={REGEXP_ONLY_DIGITS}
|
||||
onChange={(val) => setValue(val)}
|
||||
value={value}
|
||||
disabled={loading}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
{/* THIS DOESNT WORK:
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<InputOTPSlot key={i + 1} index={i + 1} />
|
||||
))}
|
||||
SO HERE'S BAD CODE YOU ARE WELCOME
|
||||
*/}
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
<InputOTPSlot index={6} />
|
||||
<InputOTPSlot index={7} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
join(value).then((res) => {
|
||||
setLoading(false);
|
||||
if (res.success) {
|
||||
toast.success('Joined project!');
|
||||
router.push(`/project/${res.id}`);
|
||||
} else {
|
||||
toast.error(res.message);
|
||||
}
|
||||
});
|
||||
}}
|
||||
loading={loading}
|
||||
>
|
||||
Let's go!
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
||||
const { id } = await params;
|
||||
const { user } = await validateRequest();
|
||||
const project = await prisma.project.findFirst({
|
||||
where: { id, userId: user!.id },
|
||||
where: { id, UserProject: { some: { userId: user!.id } } },
|
||||
include: { feedback: true },
|
||||
});
|
||||
|
||||
@@ -97,11 +97,9 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
||||
{Object.entries(JSON.parse(feedback.customData)).map(([key, value]) => (
|
||||
<TableCell key={key}>{value as string}</TableCell>
|
||||
))}
|
||||
<TableCell className='flex gap-2'>
|
||||
<TableCell className="flex gap-2">
|
||||
<FeedbackView feedback={feedback} />
|
||||
{project.github && (
|
||||
<GithubIssueCreate project={project} feedback={feedback} />
|
||||
)}
|
||||
{project.github && <GithubIssueCreate project={project} feedback={feedback} />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -6,7 +6,13 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
||||
const { id } = await params;
|
||||
const { user } = await validateRequest();
|
||||
const project = await prisma.project.findFirst({
|
||||
where: { id, userId: user!.id },
|
||||
where: { id, UserProject: { some: { userId: user!.id } } },
|
||||
include: {
|
||||
UserProject: {
|
||||
where: { projectId: id },
|
||||
include: { user: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!project) {
|
||||
return <h1>Project not found</h1>;
|
||||
|
||||
24
src/components/app/InviteCodeViewer/InviteCodeViewer.tsx
Normal file
24
src/components/app/InviteCodeViewer/InviteCodeViewer.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function InviteCodeViewer({ code }: { code: string }) {
|
||||
const [hover, setHover] = useState(false);
|
||||
return (
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Label htmlFor="email">Invite code</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="inviteCode"
|
||||
placeholder="Invite code"
|
||||
className={hover ? '' : 'blur-sm'}
|
||||
onChange={() => {}}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
value={code}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher"
|
||||
export const links = [
|
||||
{ href: '/dashboard', name: 'Dashboard' },
|
||||
{ href: '/create', name: 'Create' },
|
||||
{ href: '/join', name: 'Join' },
|
||||
]
|
||||
|
||||
function NavbarLinks() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import type { Project } from '@prisma/client';
|
||||
import type { Project, User, UserProject } from '@prisma/client';
|
||||
import { UniversalForm } from '../UniversalForm/UniversalForm';
|
||||
import {
|
||||
customData,
|
||||
@@ -23,8 +23,10 @@ import {
|
||||
import GithubRepoChooser from '../GithubRepoChooser/GithubRepoChooser';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import InviteCodeViewer from '../InviteCodeViewer/InviteCodeViewer';
|
||||
import ProjectTeamUsers from '../ProjectTeamUsers/ProjectTeamUsers';
|
||||
|
||||
export default function ProjectSettings(project: Project) {
|
||||
export default function ProjectSettings(project: ProjectWithUsers) {
|
||||
const [ghRepo, setGhRepo] = React.useState('');
|
||||
const [hasSubmitted, setHasSubmitted] = React.useState(false);
|
||||
const apiUrl = `https://${window.location.hostname}/api/feedback/${project.id}`;
|
||||
@@ -121,6 +123,41 @@ export default function ProjectSettings(project: Project) {
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Your team</CardTitle>
|
||||
<CardDescription>Invite people to join the project!</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<InviteCodeViewer code={project.inviteCode} />
|
||||
<ProjectTeamUsers UserProject={project.UserProject} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* <Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Project Deletion</CardTitle>
|
||||
<CardDescription>Permanently delete your project</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
This action is irreversible. All feedback and settings will be lost.
|
||||
</p>
|
||||
<UniversalForm
|
||||
fields={[
|
||||
{
|
||||
name: 'id',
|
||||
label: 'ID',
|
||||
type: 'hidden',
|
||||
value: project.id,
|
||||
},
|
||||
]}
|
||||
schemaName={'deleteProject'}
|
||||
action={editProject}
|
||||
submitText="Delete Project"
|
||||
submitClassname="!mt-0"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card> */}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -297,3 +334,9 @@ function stripIndents(strings: TemplateStringsArray, ...values: any[]) {
|
||||
.join('\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
interface ProjectWithUsers extends Project {
|
||||
UserProject: (UserProject & {
|
||||
user: User;
|
||||
})[];
|
||||
}
|
||||
71
src/components/app/ProjectTeamUsers/ProjectTeamUsers.tsx
Normal file
71
src/components/app/ProjectTeamUsers/ProjectTeamUsers.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import type { User, UserProject } from '@prisma/client';
|
||||
import { useSession } from '@/lib/providers/SessionProvider';
|
||||
import { deleteProjectTeamUser } from './delete';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function ProjectTeamUsers(userProject: ProjectTeamUsersProps) {
|
||||
const [users, setUsers] = useState<
|
||||
(UserProject & {
|
||||
user: User;
|
||||
})[]
|
||||
>(userProject.UserProject);
|
||||
const [loading, isLoading] = useState<string[]>([]);
|
||||
const { user: currentUser } = useSession();
|
||||
|
||||
const handleDelete = (userId: string) => {
|
||||
isLoading([...loading, userId]);
|
||||
deleteProjectTeamUser(userId, userProject.UserProject[0].projectId).then((res) => {
|
||||
if (res.success) {
|
||||
toast.success(res.message);
|
||||
setUsers(users.filter((user) => user.userId !== userId));
|
||||
} else {
|
||||
toast.error(res.message);
|
||||
}
|
||||
isLoading(loading.filter((id) => id !== userId));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ul className="space-y-2 pt-5">
|
||||
{users.map((user) => (
|
||||
<li
|
||||
key={user.userId}
|
||||
className="flex items-center justify-between p-3 rounded-lg shadow bg-accent"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src={`https://github.com/${user.user.username}.png`}
|
||||
alt={user.user.username}
|
||||
/>
|
||||
<AvatarFallback>{user.user.username.toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="font-medium">{user.user.username}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(user.userId)}
|
||||
aria-label={`Delete ${user.user.username}`}
|
||||
disabled={user.isOwner || user.userId === currentUser?.id}
|
||||
loading={loading.includes(user.userId)}
|
||||
>
|
||||
{!loading.includes(user.userId) && <Trash2 className="h-4 w-4" />}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProjectTeamUsersProps {
|
||||
UserProject: (UserProject & {
|
||||
user: User;
|
||||
})[];
|
||||
}
|
||||
24
src/components/app/ProjectTeamUsers/delete.ts
Normal file
24
src/components/app/ProjectTeamUsers/delete.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
'use server';
|
||||
|
||||
import { validateRequest } from "@/lib/auth";
|
||||
import prisma from "@/lib/db";
|
||||
|
||||
export async function deleteProjectTeamUser(userId: string, projectId: string) {
|
||||
const { user } = await validateRequest();
|
||||
if (!user) {
|
||||
return { success: false, message: 'Unauthorized' };
|
||||
}
|
||||
|
||||
const userProject = await prisma.userProject.findFirst({
|
||||
where: { userId, projectId },
|
||||
include: { user: true },
|
||||
});
|
||||
if (!userProject) {
|
||||
return { success: false, message: 'User not found' };
|
||||
}
|
||||
|
||||
await prisma.userProject.deleteMany({
|
||||
where: { userId, projectId },
|
||||
});
|
||||
return { success: true, message: `User ${userProject.user.username} removed.` };
|
||||
}
|
||||
71
src/components/ui/input-otp.tsx
Normal file
71
src/components/ui/input-otp.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { Dot } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
||||
containerClassName
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
InputOTP.displayName = "InputOTP"
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
))
|
||||
InputOTPGroup.displayName = "InputOTPGroup"
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
InputOTPSlot.displayName = "InputOTPSlot"
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
))
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PrismaAdapter } from '@lucia-auth/adapter-prisma';
|
||||
import { Lucia, Session, User } from 'lucia';
|
||||
import { Lucia } from 'lucia';
|
||||
import prisma from '../db';
|
||||
import { cache } from 'react';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
@@ -27,9 +27,10 @@ export async function create(prev: any, formData: FormData) {
|
||||
const dbCreate = await prisma.project.create({
|
||||
data: {
|
||||
...zod.data,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
UserProject: {
|
||||
create: {
|
||||
userId: user.id,
|
||||
isOwner: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -141,7 +142,14 @@ export async function githubTestIssue(prev: any, formData: FormData) {
|
||||
id: zod.data.id,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
UserProject: {
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
if (!project) {
|
||||
@@ -152,7 +160,7 @@ export async function githubTestIssue(prev: any, formData: FormData) {
|
||||
const [owner, repo] = project.github!.split('/').slice(-2);
|
||||
let issueCreated = false;
|
||||
|
||||
for (const installationId of project.user.installations) {
|
||||
for (const installationId of project.UserProject[0].user.installations) {
|
||||
if (issueCreated) break;
|
||||
|
||||
const installation = await octokitApp.getInstallationOctokit(Number(installationId));
|
||||
@@ -199,7 +207,14 @@ export async function githubCreateIssue(prev: any, formData: FormData) {
|
||||
id: zod.data.project,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
UserProject: {
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!project) {
|
||||
@@ -209,7 +224,7 @@ export async function githubCreateIssue(prev: any, formData: FormData) {
|
||||
try {
|
||||
const [owner, repo] = project.github!.split('/').slice(-2);
|
||||
|
||||
for (const installationId of project.user.installations) {
|
||||
for (const installationId of project.UserProject[0].user.installations) {
|
||||
const installation = await octokitApp.getInstallationOctokit(Number(installationId));
|
||||
const getRepo = await installation
|
||||
.request('GET /repos/{owner}/{repo}', {
|
||||
|
||||
@@ -67,10 +67,15 @@ const config = {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
"caret-blink": {
|
||||
"0%,70%,100%": { opacity: "1" },
|
||||
"20%,50%": { opacity: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3579,6 +3579,11 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
input-otp@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/input-otp/-/input-otp-1.4.1.tgz#bc22e68b14b1667219d54adf74243e37ea79cf84"
|
||||
integrity sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==
|
||||
|
||||
internal-slot@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
|
||||
|
||||
Reference in New Issue
Block a user