Compare commits
10 Commits
6377e46c5c
...
dd98781cc5
Author | SHA1 | Date |
---|---|---|
|
dd98781cc5 | |
|
40744a0502 | |
|
44891b9d87 | |
|
47fbed74c5 | |
|
d6ba4e7c5a | |
|
f57e0eed3c | |
|
d689f9a7a5 | |
|
8d6bbe13de | |
|
df54fb164c | |
|
5b1d5266f8 |
|
@ -0,0 +1,39 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export type NewApplication = {
|
||||||
|
serviceProvider: string;
|
||||||
|
git: {
|
||||||
|
repositoryId: number;
|
||||||
|
repositoryName: string;
|
||||||
|
branch: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
autodeploy: boolean;
|
||||||
|
env: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createApplication(app: NewApplication, workspace: string) {
|
||||||
|
const application = await prisma.application.create({
|
||||||
|
data: {
|
||||||
|
name: app.name,
|
||||||
|
autoDeploy: app.autodeploy,
|
||||||
|
branch: app.git.branch,
|
||||||
|
path: app.git.path,
|
||||||
|
repositoryId: app.git.repositoryId.toString(),
|
||||||
|
repository: app.git.repositoryName,
|
||||||
|
serviceProvider: app.serviceProvider,
|
||||||
|
Workspace: {
|
||||||
|
connect: {
|
||||||
|
slug: workspace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return application;
|
||||||
|
}
|
|
@ -46,3 +46,16 @@ export async function getUserRepository(userId: string, username: string): Promi
|
||||||
|
|
||||||
return repositories.repositories as GithubRepository[];
|
return repositories.repositories as GithubRepository[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getRepositoryBranches(userId: string, username: string, repository: string): Promise<string[]> {
|
||||||
|
const branches = await fetch(`https://api.github.com/repos/${username}/${repository}/branches`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${await getInstallationAccessToken(userId, username)}`,
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
},
|
||||||
|
method: 'GET',
|
||||||
|
}).then(res => res.json());
|
||||||
|
|
||||||
|
return branches.map((branch: { name: string }) => branch.name) as string[];
|
||||||
|
}
|
|
@ -2,11 +2,19 @@
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { Plus, Link as LinkIcon } from 'lucide-react';
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { getRepositoryBranches, getUserRepository, GithubRepository } from '@/actions/github/repository';
|
||||||
|
import { signOut, useSession } from 'next-auth/react';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { createApplication, NewApplication } from '@/actions/deploy/application';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export const ServiceProviderList = [
|
export const ServiceProviderList = [
|
||||||
{
|
{
|
||||||
|
@ -28,10 +36,52 @@ export const ServiceProviderList = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function CreateApplicationForm() {
|
export default function CreateApplicationForm() {
|
||||||
const [serviceProvider, setServiceProvider] = useState<string>('github');
|
const [steps, setSteps] = useState(1);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
const [repositories, setRepositories] = useState<GithubRepository[]>([]);
|
||||||
|
const [branches, setBranches] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [newApplication, setNewApplication] = useState<NewApplication>({
|
||||||
|
serviceProvider: 'github',
|
||||||
|
git: {
|
||||||
|
repositoryId: 0,
|
||||||
|
repositoryName: '',
|
||||||
|
branch: '',
|
||||||
|
path: '',
|
||||||
|
},
|
||||||
|
autodeploy: true,
|
||||||
|
env: {},
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchRepositories() {
|
||||||
|
if (newApplication.serviceProvider == 'github') {
|
||||||
|
const repos = await getUserRepository(session?.user.id as string, session?.user.username as string);
|
||||||
|
setRepositories(repos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRepositories();
|
||||||
|
}, [newApplication.serviceProvider, session]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchBranches() {
|
||||||
|
if (newApplication.serviceProvider == 'github' && newApplication.git.repositoryId != 0) {
|
||||||
|
const branches = await getRepositoryBranches(session?.user.id as string, session?.user.username as string, newApplication.git.repositoryName);
|
||||||
|
setBranches(branches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchBranches();
|
||||||
|
}, [newApplication.serviceProvider, newApplication.git.repositoryId, newApplication.git.repositoryName, session]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet open={open} onOpenChange={() => setOpen((open) => !open)}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button className="flex gap-1 justify-center items-center bg-[#3A7BFE]">
|
<Button className="flex gap-1 justify-center items-center bg-[#3A7BFE]">
|
||||||
<Plus />
|
<Plus />
|
||||||
|
@ -40,14 +90,25 @@ export default function CreateApplicationForm() {
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className="min-w-[500px]">
|
<SheetContent className="min-w-[500px]">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>New Application</SheetTitle>
|
<SheetTitle className="text-2xl">New Application</SheetTitle>
|
||||||
<SheetDescription>Deploy a new application from source</SheetDescription>
|
<SheetDescription>Deploy a new application from source</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-col gap-2 my-6">
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<p>
|
||||||
|
Step: <span className="text-muted-foreground">{(steps == 1 && 'Select Provider') || (steps == 2 && 'Resources') || (steps == 3 && 'Environment Variables') || (steps == 4 && 'Information') || (steps == 5 && 'Review')}</span>
|
||||||
|
</p>
|
||||||
|
<p>{steps} / 5</p>
|
||||||
|
</div>
|
||||||
|
<Progress className="bg-gray-300 h-3 " color="red" value={steps * 20} max={100} indicatorColor="bg-[#3A7BFE]" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>Select Provider</h3>
|
{steps == 1 && (
|
||||||
<RadioGroup defaultValue="github" className="flex flex-col gap-4" onValueChange={setServiceProvider}>
|
<div>
|
||||||
|
<RadioGroup defaultValue={newApplication.serviceProvider || 'github'} className="flex flex-col gap-2 mb-4" onValueChange={(e) => setNewApplication((prev) => ({ ...prev, serviceProvider: e }))}>
|
||||||
{ServiceProviderList.map((provider) => (
|
{ServiceProviderList.map((provider) => (
|
||||||
<div key={provider.value} className={cn('flex flex-row items-center gap-4 border-[1px] rounded-lg py-2 px-4', provider.value == serviceProvider ? 'border-[#3A7BFE]' : 'border-gray-300')}>
|
<div key={provider.value} className={cn('flex flex-row items-center gap-4 border-[1px] rounded-lg py-2 px-4', provider.value == newApplication.serviceProvider ? 'border-[#3A7BFE]' : 'border-gray-300')}>
|
||||||
<RadioGroupItem value={provider.value} id={provider.value} />
|
<RadioGroupItem value={provider.value} id={provider.value} />
|
||||||
<label htmlFor={provider.value} className="flex flex-row items-center gap-4">
|
<label htmlFor={provider.value} className="flex flex-row items-center gap-4">
|
||||||
<img src={provider.image} alt={provider.name} className="w-8 h-8 rounded-full" />
|
<img src={provider.image} alt={provider.name} className="w-8 h-8 rounded-full" />
|
||||||
|
@ -56,11 +117,177 @@ export default function CreateApplicationForm() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
{(serviceProvider == 'github' || serviceProvider == 'github-registry') && (
|
<div className="my-2 flex flex-col gap-2">
|
||||||
<p>
|
{newApplication.serviceProvider == 'github' && !!repositories && (
|
||||||
Not seeing the repositories you expected here? <Link href={ServiceProviderList.find((s) => s.value == serviceProvider)?.permission || ''}>Edit Your GitHub Permissions</Link>
|
<div>
|
||||||
</p>
|
<p className="text-muted-foreground text-sm">Repository</p>
|
||||||
|
<Select defaultValue={newApplication.git.repositoryId.toString() || ''} onValueChange={(e) => setNewApplication((prev) => ({ ...prev, git: { ...prev.git, repositoryId: parseInt(e), repositoryName: repositories.find((w) => w.id == parseInt(e))?.name as string } }))}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a repository" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{repositories?.map((repository) => (
|
||||||
|
<SelectItem key={repository.id} value={repository.id.toString()}>
|
||||||
|
{repository.full_name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{newApplication.serviceProvider == 'github' && newApplication.git.repositoryId != 0 && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-sm">Branch</p>
|
||||||
|
<Select onValueChange={(e) => setNewApplication((prev) => ({ ...prev, git: { ...prev.git, branch: e } }))} defaultValue={newApplication.git.branch}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a branch" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{branches.map((branch) => (
|
||||||
|
<SelectItem key={branch} value={branch}>
|
||||||
|
{branch}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-sm">Path</p>
|
||||||
|
<Input placeholder="Path" value={newApplication.git.path} onChange={(e) => setNewApplication((prev) => ({ ...prev, git: { ...prev.git, path: e.target.value } }))} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={cn('flex flex-row items-center gap-4 border-[1px] rounded-lg py-2 px-4', newApplication.autodeploy ? 'border-[#3A7BFE]' : 'border-gray-300')} onClick={() => setNewApplication((prev) => ({ ...prev, autodeploy: !prev.autodeploy }))}>
|
||||||
|
<Checkbox
|
||||||
|
checked={newApplication.autodeploy}
|
||||||
|
onChange={(e) => {
|
||||||
|
console.log(e);
|
||||||
|
}}
|
||||||
|
id={'autodeploy'}
|
||||||
|
color="bg-[#3A7BFE]"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="autodeploy" className="text-sm">
|
||||||
|
Auto Deploy
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground">Every time an update is made to this branch, your application will be re-deployed.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{newApplication.serviceProvider == 'github' &&
|
||||||
|
(!repositories ? (
|
||||||
|
<p className="text-sm">
|
||||||
|
<span className="text-muted-foreground">We don't have access to your repositories.</span> <Link href={ServiceProviderList.find((s) => s.value == newApplication.serviceProvider)?.permission || ''}>Link your GitHub account</Link>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm">
|
||||||
|
<span className="text-muted-foreground">Not seeing the repositories you expected here?</span> <Link href={ServiceProviderList.find((s) => s.value == newApplication.serviceProvider)?.permission || ''}>Edit Your GitHub Permissions</Link>
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{steps == 2 && (
|
||||||
|
<div>
|
||||||
|
<p>Resources WIP</p>
|
||||||
|
<p>For now 1 vCPU & 1024Mb RAM</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{steps == 3 && (
|
||||||
|
<div>
|
||||||
|
<p>Environment Variables WIP</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{steps == 4 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-sm">Application Name</p>
|
||||||
|
<Input placeholder="App Name" value={newApplication.name} onChange={(e) => setNewApplication((prev) => ({ ...prev, name: e.target.value }))} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{steps == 5 && (
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-sm">Service Provider</p>
|
||||||
|
<p>{newApplication.serviceProvider}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-sm">Repository</p>
|
||||||
|
<p>{newApplication.git.repositoryName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-sm">Branch</p>
|
||||||
|
<p>{newApplication.git.branch}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-sm">Path</p>
|
||||||
|
<p>{newApplication.git.path}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-sm">Auto Deploy</p>
|
||||||
|
<p>{newApplication.autodeploy ? 'Enabled' : 'Disabled'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground text-sm">Application Name</p>
|
||||||
|
<p>{newApplication.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-between my-6">
|
||||||
|
<Button
|
||||||
|
variant={'outline'}
|
||||||
|
className={cn(steps == 1 && 'opacity-0 pointer-events-none select-none')}
|
||||||
|
onClick={() => {
|
||||||
|
setSteps((prev) => (prev - 1 < 1 ? 1 : prev - 1));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
{steps == 5 ? (
|
||||||
|
<Button
|
||||||
|
className="bg-[#3A7BFE]"
|
||||||
|
onClick={async () => {
|
||||||
|
const app = await createApplication(newApplication, params.workspace as string);
|
||||||
|
console.log(app);
|
||||||
|
setOpen(false);
|
||||||
|
setNewApplication({
|
||||||
|
serviceProvider: 'github',
|
||||||
|
git: {
|
||||||
|
repositoryId: 0,
|
||||||
|
repositoryName: '',
|
||||||
|
branch: '',
|
||||||
|
path: '',
|
||||||
|
},
|
||||||
|
autodeploy: true,
|
||||||
|
env: {},
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
className="bg-[#3A7BFE]"
|
||||||
|
onClick={() => {
|
||||||
|
setSteps((prev) => (prev + 1 > 5 ? 5 : prev + 1));
|
||||||
|
console.log(newApplication);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { Button } from '@/components/ui/button';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import { PackagePlus, Plus } from 'lucide-react';
|
import { PackagePlus, Plus } from 'lucide-react';
|
||||||
import CreateApplicationForm from './CreateApplicationForm';
|
import CreateApplicationForm from './CreateApplicationForm';
|
||||||
import { getSession } from 'next-auth/react';
|
|
||||||
|
|
||||||
export default async function Workspace({ params }: { params: { workspace: string } }) {
|
export default async function Workspace({ params }: { params: { workspace: string } }) {
|
||||||
const applications = await prisma.application.findMany({
|
const applications = await prisma.application.findMany({
|
||||||
|
@ -13,21 +12,6 @@ export default async function Workspace({ params }: { params: { workspace: strin
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const session = await getSession();
|
|
||||||
const account = await prisma.account.findFirst({
|
|
||||||
where: {
|
|
||||||
userId: session?.user.id as string,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
fetch('https://api.github.com/users/fayorg/repos', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${account?.access_token}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then(console.log);
|
|
||||||
|
|
||||||
if (applications.length == 0) {
|
if (applications.length == 0) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { redirect } from 'next/navigation';
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import WorkspaceNavigation from './WorkspaceNavigation';
|
import WorkspaceNavigation from './WorkspaceNavigation';
|
||||||
|
import { signOut } from 'next-auth/react';
|
||||||
|
|
||||||
export default async function DashboardLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
export default async function DashboardLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||||
const session = await getServerSession();
|
const session = await getServerSession();
|
||||||
|
@ -12,6 +13,9 @@ export default async function DashboardLayout({ children }: Readonly<{ children:
|
||||||
if (!session) {
|
if (!session) {
|
||||||
redirect('/sign-in');
|
redirect('/sign-in');
|
||||||
}
|
}
|
||||||
|
if (session.error == 'RefreshAccessTokenError') {
|
||||||
|
signOut();
|
||||||
|
}
|
||||||
|
|
||||||
const workspaces = await prisma.workspace.findMany({
|
const workspaces = await prisma.workspace.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root ref={ref} className={cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', className)} {...props}>
|
||||||
|
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
));
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Checkbox };
|
|
@ -0,0 +1,25 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
|
@ -0,0 +1,18 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface CustomProgressProps extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
|
||||||
|
indicatorColor: string;
|
||||||
|
}
|
||||||
|
const Progress = React.forwardRef<React.ElementRef<typeof ProgressPrimitive.Root>, CustomProgressProps>(({ className, value, indicatorColor, ...props }, ref) => (
|
||||||
|
<ProgressPrimitive.Root ref={ref} className={cn('relative h-4 w-full overflow-hidden rounded-full bg-secondary', className)} {...props}>
|
||||||
|
<ProgressPrimitive.Indicator className={`h-full w-full flex-1 transition-all ${indicatorColor}`} style={{ transform: `translateX(-${100 - (value || 0)}%)` }} />
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
));
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Progress };
|
|
@ -0,0 +1,78 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||||
|
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root;
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group;
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value;
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger ref={ref} className={cn('flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 [&>span]:line-clamp-1', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
));
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton ref={ref} className={cn('flex cursor-default items-center justify-center py-1', className)} {...props}>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
));
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton ref={ref} className={cn('flex cursor-default items-center justify-center py-1', className)} {...props}>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
));
|
||||||
|
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Content>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
position === 'popper' && 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport className={cn('p-1', position === 'popper' && 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]')}>{children}</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
));
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>(({ className, ...props }, ref) => <SelectPrimitive.Label ref={ref} className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)} {...props} />);
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item ref={ref} className={cn('relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground', className)} {...props}>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
));
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Separator>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>>(({ className, ...props }, ref) => <SelectPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />);
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton };
|
|
@ -15,7 +15,7 @@ export const authOptions: NextAuthOptions = {
|
||||||
name: profile.name,
|
name: profile.name,
|
||||||
email: profile.email,
|
email: profile.email,
|
||||||
image: profile.avatar_url,
|
image: profile.avatar_url,
|
||||||
username: profile.username,
|
username: profile.login,
|
||||||
} as User;
|
} as User;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -35,25 +35,28 @@ export const authOptions: NextAuthOptions = {
|
||||||
if ((github.expires_at || 0) * 1000 < Date.now()) {
|
if ((github.expires_at || 0) * 1000 < Date.now()) {
|
||||||
// If the access token has expired, try to refresh it
|
// If the access token has expired, try to refresh it
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://github.com/login/oauth/access_token", {
|
|
||||||
headers: { "Content-Type": "application/json" },
|
const params = new URLSearchParams({
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: process.env.GITHUB_ID as string,
|
client_id: process.env.GITHUB_ID as string,
|
||||||
client_secret: process.env.GITHUB_SECRET as string,
|
client_secret: process.env.GITHUB_SECRET as string,
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
refresh_token: github.refresh_token as string,
|
refresh_token: github.refresh_token as string,
|
||||||
}),
|
});
|
||||||
|
|
||||||
|
const response = await fetch("https://github.com/login/oauth/access_token?" + params, {
|
||||||
|
headers: { "Accept": "application/json" },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
})
|
})
|
||||||
|
|
||||||
const tokens: TokenSet = await response.json()
|
const tok = await response.json()
|
||||||
|
const tokens: TokenSet = tok;
|
||||||
|
|
||||||
if (!response.ok) throw tokens
|
if (!response.ok) throw tokens
|
||||||
|
|
||||||
await prisma.account.update({
|
await prisma.account.update({
|
||||||
data: {
|
data: {
|
||||||
access_token: tokens.access_token,
|
access_token: tokens.access_token,
|
||||||
expires_at: Math.floor(Date.now() / 1000 + (tokens.expires_at as number)),
|
expires_at: Math.floor(Date.now() / 1000 + (tokens.expires_in as number)),
|
||||||
refresh_token: tokens.refresh_token ?? github.refresh_token,
|
refresh_token: tokens.refresh_token ?? github.refresh_token,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
|
@ -87,7 +90,6 @@ export const authOptions: NextAuthOptions = {
|
||||||
username: u.username,
|
username: u.username,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
console.log(token);
|
|
||||||
return { ...token, username: token.username };
|
return { ...token, username: token.username };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,13 +12,17 @@
|
||||||
"@auth/prisma-adapter": "^1.5.0",
|
"@auth/prisma-adapter": "^1.5.0",
|
||||||
"@prisma/client": "^5.11.0",
|
"@prisma/client": "^5.11.0",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"next": "14.1.4",
|
"next": "14.1.4",
|
||||||
"next-auth": "^4.24.7",
|
"next-auth": "^4.24.7",
|
||||||
|
@ -28,6 +32,7 @@
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
@ -615,6 +620,14 @@
|
||||||
"@prisma/debug": "5.11.0"
|
"@prisma/debug": "5.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/number": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
|
||||||
|
@ -672,6 +685,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-presence": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.0.1",
|
||||||
|
"@radix-ui/react-use-size": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-collection": {
|
"node_modules/@radix-ui/react-collection": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
|
||||||
|
@ -1011,6 +1054,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-progress": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-radio-group": {
|
"node_modules/@radix-ui/react-radio-group": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz",
|
||||||
|
@ -1074,6 +1141,49 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-select": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/number": "1.0.1",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-collection": "1.0.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.0.5",
|
||||||
|
"@radix-ui/react-focus-guards": "1.0.1",
|
||||||
|
"@radix-ui/react-focus-scope": "1.0.4",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-popper": "1.1.3",
|
||||||
|
"@radix-ui/react-portal": "1.0.4",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-slot": "1.0.2",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.0.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.0.1",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.0.3",
|
||||||
|
"aria-hidden": "^1.1.1",
|
||||||
|
"react-remove-scroll": "2.5.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||||
|
@ -1215,6 +1325,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-visually-hidden": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/rect": {
|
"node_modules/@radix-ui/rect": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz",
|
||||||
|
@ -1248,6 +1381,15 @@
|
||||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jsonwebtoken": {
|
||||||
|
"version": "9.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz",
|
||||||
|
"integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.11.30",
|
"version": "20.11.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
|
||||||
|
@ -1842,6 +1984,11 @@
|
||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||||
|
},
|
||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
|
@ -2238,6 +2385,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.715",
|
"version": "1.4.715",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz",
|
||||||
|
@ -3855,6 +4010,27 @@
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^3.2.2",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsx-ast-utils": {
|
"node_modules/jsx-ast-utils": {
|
||||||
"version": "3.3.5",
|
"version": "3.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||||
|
@ -3870,6 +4046,25 @@
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^1.4.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
@ -3938,12 +4133,47 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
@ -4023,8 +4253,7 @@
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/mz": {
|
"node_modules/mz": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
|
@ -5034,6 +5263,25 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/safe-regex-test": {
|
"node_modules/safe-regex-test": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
|
||||||
|
@ -5063,7 +5311,6 @@
|
||||||
"version": "7.6.0",
|
"version": "7.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
||||||
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
},
|
},
|
||||||
|
@ -5078,7 +5325,6 @@
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,13 +12,17 @@
|
||||||
"@auth/prisma-adapter": "^1.5.0",
|
"@auth/prisma-adapter": "^1.5.0",
|
||||||
"@prisma/client": "^5.11.0",
|
"@prisma/client": "^5.11.0",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"next": "14.1.4",
|
"next": "14.1.4",
|
||||||
"next-auth": "^4.24.7",
|
"next-auth": "^4.24.7",
|
||||||
|
@ -28,6 +32,7 @@
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
|
|
@ -51,6 +51,7 @@ model User {
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
Workspace Workspace[]
|
Workspace Workspace[]
|
||||||
|
Deployment Deployment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
|
@ -71,13 +72,43 @@ model Workspace {
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
Application Application[]
|
Application Application[]
|
||||||
|
Deployment Deployment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Application {
|
model Application {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
Workspace Workspace @relation(fields: [workspaceId], references: [id])
|
serviceProvider String
|
||||||
|
repository String
|
||||||
|
branch String
|
||||||
|
repositoryId String
|
||||||
|
path String
|
||||||
|
autoDeploy Boolean
|
||||||
workspaceId String
|
workspaceId String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
Deployment Deployment[]
|
||||||
|
|
||||||
|
Workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DeploymentStatus {
|
||||||
|
PENDING
|
||||||
|
IN_PROGRESS
|
||||||
|
FAILED
|
||||||
|
SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
model Deployment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
applicationId String
|
||||||
|
workspaceId String
|
||||||
|
userId String
|
||||||
|
status DeploymentStatus @default(PENDING)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
Application Application @relation(fields: [applicationId], references: [id], onDelete: Cascade)
|
||||||
|
Workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue