Progress so far
This commit is contained in:
		
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -34,3 +34,7 @@ yarn-error.log*
 | 
				
			|||||||
# typescript
 | 
					# typescript
 | 
				
			||||||
*.tsbuildinfo
 | 
					*.tsbuildinfo
 | 
				
			||||||
next-env.d.ts
 | 
					next-env.d.ts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					package.json
 | 
				
			||||||
 | 
					package-lock.json
 | 
				
			||||||
							
								
								
									
										62
									
								
								app/(modal)/(routes)/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								app/(modal)/(routes)/page.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { redirect } from "next/navigation";
 | 
				
			||||||
 | 
					import Link from "next/link";
 | 
				
			||||||
 | 
					import { useSession, signIn, signOut } from "next-auth/react";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Home() {
 | 
				
			||||||
 | 
					    const { data: session, status } = useSession();
 | 
				
			||||||
 | 
					    const [previousUsername, setPreviousUsername] = useState<string>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // if (status !== "authenticated") {
 | 
				
			||||||
 | 
					    //     redirect('/api/auth/signin?redirectUrl=/');
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        if (status !== "authenticated" || previousUsername == session.user?.name) {
 | 
				
			||||||
 | 
					            console.log("CANCELED")
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setPreviousUsername(session.user?.name as string)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        async function saveAccount() {
 | 
				
			||||||
 | 
					            const data = await axios.post("/api/account")
 | 
				
			||||||
 | 
					            if (data == null || data == undefined) {
 | 
				
			||||||
 | 
					              console.log("ERROR")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        saveAccount().catch(console.error)
 | 
				
			||||||
 | 
					    }, [session])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <main
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        display: "flex",
 | 
				
			||||||
 | 
					        justifyContent: "center",
 | 
				
			||||||
 | 
					        alignItems: "center",
 | 
				
			||||||
 | 
					        height: "70vh",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div className="header">
 | 
				
			||||||
 | 
					        <Link href="/">
 | 
				
			||||||
 | 
					          <p className="logo">NextAuth.js</p>
 | 
				
			||||||
 | 
					        </Link>
 | 
				
			||||||
 | 
					        {session && (
 | 
				
			||||||
 | 
					          <Link href="#" onClick={() => signOut()} className="btn-signin">
 | 
				
			||||||
 | 
					            Sign out
 | 
				
			||||||
 | 
					          </Link>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {!session && (
 | 
				
			||||||
 | 
					          <Link href="#" onClick={() => signIn()} className="btn-signin">
 | 
				
			||||||
 | 
					            Sign in
 | 
				
			||||||
 | 
					          </Link>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </main>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										223
									
								
								app/(modal)/(routes)/settings/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								app/(modal)/(routes)/settings/page.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,223 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
 | 
				
			||||||
 | 
					import { Info } from "lucide-react";
 | 
				
			||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Toggle } from "@/components/ui/toggle";
 | 
				
			||||||
 | 
					import { ApiKey, TwitchConnection, User } from "@prisma/client";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { useSession } from "next-auth/react";
 | 
				
			||||||
 | 
					import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
 | 
				
			||||||
 | 
					import Link from "next/link";
 | 
				
			||||||
 | 
					import { TTSBadgeFilterModal } from "@/components/modals/tts-badge-filter-modal";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SettingsPage = () => {
 | 
				
			||||||
 | 
					    const { data: session, status } = useSession();
 | 
				
			||||||
 | 
					    const [previousUsername, setPreviousUsername] = useState<string>()
 | 
				
			||||||
 | 
					    const [userId, setUserId] = useState<string>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					      if (status !== "authenticated" || previousUsername == session.user?.name) {
 | 
				
			||||||
 | 
					          return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setPreviousUsername(session.user?.name as string)
 | 
				
			||||||
 | 
					      if (session.user?.name) {
 | 
				
			||||||
 | 
					        const fetchData = async () => {
 | 
				
			||||||
 | 
					          var connection: User = (await axios.get("/api/account")).data
 | 
				
			||||||
 | 
					          setUserId(connection.id)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fetchData().catch(console.error)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }, [session])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // const [twitchUser, setTwitchUser] = useState<TwitchConnection | null>(null)
 | 
				
			||||||
 | 
					    // useEffect(() => {
 | 
				
			||||||
 | 
					    //     const fetchData = async () => {
 | 
				
			||||||
 | 
					    //         var connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data
 | 
				
			||||||
 | 
					    //         setTwitchUser(connection)
 | 
				
			||||||
 | 
					    //     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //     fetchData().catch(console.error);
 | 
				
			||||||
 | 
					    // }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // const OnTwitchConnectionDelete = async () => {
 | 
				
			||||||
 | 
					    //     try {
 | 
				
			||||||
 | 
					    //         await axios.post("/api/settings/connections/twitch/delete")
 | 
				
			||||||
 | 
					    //         setTwitchUser(null)
 | 
				
			||||||
 | 
					    //     } catch (error) {
 | 
				
			||||||
 | 
					    //         console.log("ERROR", error)
 | 
				
			||||||
 | 
					    //     }
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [apiKeyViewable, setApiKeyViewable] = useState(0)
 | 
				
			||||||
 | 
					    const [apiKeyChanges, setApiKeyChanges] = useState(0)
 | 
				
			||||||
 | 
					    const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					      const fetchData = async () => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const keys = (await axios.get("/api/tokens")).data ?? {};
 | 
				
			||||||
 | 
					          setApiKeys(keys)
 | 
				
			||||||
 | 
					          console.log(keys);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          console.log("ERROR", error)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fetchData().catch(console.error);
 | 
				
			||||||
 | 
					    }, [apiKeyChanges]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onApiKeyAdd = async () => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        await axios.post("/api/token", {
 | 
				
			||||||
 | 
					          label: "Key label"
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        setApiKeyChanges(apiKeyChanges + 1)
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.log("ERROR", error)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onApiKeyDelete = async (id: string) => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        await axios.delete("/api/token/" + id);
 | 
				
			||||||
 | 
					        setApiKeyChanges(apiKeyChanges - 1)
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.log("ERROR", error)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					      const fetchData = async () => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const keys = (await axios.get("/api/tokens")).data;
 | 
				
			||||||
 | 
					          setApiKeys(keys)
 | 
				
			||||||
 | 
					          console.log(keys);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          console.log("ERROR", error)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fetchData().catch(console.error);
 | 
				
			||||||
 | 
					    }, [apiKeyViewable]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            <div className="flex text-3xl justify-center">
 | 
				
			||||||
 | 
					                Settings
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="border-solid border-white px-10 py-5 mx-5 my-10">
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                    <div className="text-xl justify-left">Connections</div>
 | 
				
			||||||
 | 
					                    <div className="px-10 py-6 rounded-md bg-purple-500 max-w-sm overflow-hidden wrap-">
 | 
				
			||||||
 | 
					                        <div className="inline-block max-w-md">
 | 
				
			||||||
 | 
					                            <Avatar>
 | 
				
			||||||
 | 
					                                <AvatarImage src="https://cdn2.iconfinder.com/data/icons/social-aquicons/512/Twitch.png" alt="twitch" />
 | 
				
			||||||
 | 
					                                <AvatarFallback></AvatarFallback>
 | 
				
			||||||
 | 
					                            </Avatar>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        {/* <div className="inline-block ml-5"> */}
 | 
				
			||||||
 | 
					                          <Link href={(process.env.NEXT_PUBLIC_TWITCH_OAUTH_URL as string) + userId}>Authorize</Link>
 | 
				
			||||||
 | 
					                          {/* <div className="inline-block text-lg">Twitch</div>
 | 
				
			||||||
 | 
					                            <div className={cn("hidden", twitchUser == null && "flex")}>
 | 
				
			||||||
 | 
					                                <ConnectTwitchModal />
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            <div className={cn("hidden", twitchUser != null && "flex")}>
 | 
				
			||||||
 | 
					                                <p>{twitchUser?.username}</p>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div className="inline-block pl-5 ml-3 justify-right items-right">
 | 
				
			||||||
 | 
					                            <button onClick={OnTwitchConnectionDelete} className={cn("hidden", twitchUser != null && "flex")}>
 | 
				
			||||||
 | 
					                                <Avatar>
 | 
				
			||||||
 | 
					                                    <AvatarImage src="https://upload.wikimedia.org/wikipedia/en/b/ba/Red_x.svg" alt="twitch" />
 | 
				
			||||||
 | 
					                                    <AvatarFallback></AvatarFallback>
 | 
				
			||||||
 | 
					                                </Avatar>
 | 
				
			||||||
 | 
					                            </button>
 | 
				
			||||||
 | 
					                        </div> */}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                  <div className="text-xl justify-left mt-10">API Keys</div>
 | 
				
			||||||
 | 
					                    <Table className="max-w-2xl">
 | 
				
			||||||
 | 
					                      <TableCaption>A list of your secret API keys.</TableCaption>
 | 
				
			||||||
 | 
					                      <TableHeader>
 | 
				
			||||||
 | 
					                        <TableRow>
 | 
				
			||||||
 | 
					                          <TableHead>Label</TableHead>
 | 
				
			||||||
 | 
					                          <TableHead>Token</TableHead>
 | 
				
			||||||
 | 
					                          <TableHead>View</TableHead>
 | 
				
			||||||
 | 
					                          <TableHead>Action</TableHead>
 | 
				
			||||||
 | 
					                        </TableRow>
 | 
				
			||||||
 | 
					                      </TableHeader>
 | 
				
			||||||
 | 
					                      <TableBody>
 | 
				
			||||||
 | 
					                        {apiKeys.map((key, index) => (
 | 
				
			||||||
 | 
					                          <TableRow key={key.id}>
 | 
				
			||||||
 | 
					                            <TableCell className="font-medium">{key.label}</TableCell>
 | 
				
			||||||
 | 
					                            <TableCell>{(apiKeyViewable & (1 << index)) > 0 ? key.id : "*".repeat(key.id.length)}</TableCell>
 | 
				
			||||||
 | 
					                            <TableCell>
 | 
				
			||||||
 | 
					                              <Button onClick={() => setApiKeyViewable((v) => v ^ (1 << index))}>
 | 
				
			||||||
 | 
					                                {(apiKeyViewable & (1 << index)) > 0 ? "HIDE" : "VIEW"}
 | 
				
			||||||
 | 
					                              </Button>
 | 
				
			||||||
 | 
					                            </TableCell>
 | 
				
			||||||
 | 
					                            <TableCell><Button onClick={() => onApiKeyDelete(key.id)}>DEL</Button></TableCell>
 | 
				
			||||||
 | 
					                          </TableRow>
 | 
				
			||||||
 | 
					                        ))}
 | 
				
			||||||
 | 
					                        <TableRow key="ADD">
 | 
				
			||||||
 | 
					                          <TableCell className="font-medium"></TableCell>
 | 
				
			||||||
 | 
					                          <TableCell></TableCell>
 | 
				
			||||||
 | 
					                          <TableCell></TableCell>
 | 
				
			||||||
 | 
					                          <TableCell><Button onClick={onApiKeyAdd}>ADD</Button></TableCell>
 | 
				
			||||||
 | 
					                        </TableRow>
 | 
				
			||||||
 | 
					                      </TableBody>
 | 
				
			||||||
 | 
					                    </Table>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            <div className="px-10 py-5 mx-5 my-10 max-w-lg inline-block">
 | 
				
			||||||
 | 
					                <div className="text-xl justify-left">
 | 
				
			||||||
 | 
					                    <Info className="h-4 w-4 mx-1 inline" />
 | 
				
			||||||
 | 
					                    TTS Voice
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div className="px-10 py-6 rounded-md bg-pink-300 max-w-sm">
 | 
				
			||||||
 | 
					                    <DropdownMenu>
 | 
				
			||||||
 | 
					                        <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					                            <Button variant="outline">Default Voice</Button>
 | 
				
			||||||
 | 
					                        </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					                        <DropdownMenuContent className="w-56">
 | 
				
			||||||
 | 
					                            <DropdownMenuLabel>English Voices</DropdownMenuLabel>
 | 
				
			||||||
 | 
					                            <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					                            <DropdownMenuRadioGroup value="voice">
 | 
				
			||||||
 | 
					                                <DropdownMenuRadioItem value="brian">Brian</DropdownMenuRadioItem>
 | 
				
			||||||
 | 
					                                <DropdownMenuRadioItem value="amy">Amy</DropdownMenuRadioItem>
 | 
				
			||||||
 | 
					                                <DropdownMenuRadioItem value="emma">Emma</DropdownMenuRadioItem>
 | 
				
			||||||
 | 
					                            </DropdownMenuRadioGroup>
 | 
				
			||||||
 | 
					                        </DropdownMenuContent>
 | 
				
			||||||
 | 
					                    </DropdownMenu>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div className="px-10 py-5 mx-5 my-10 max-w-lg inline-block">
 | 
				
			||||||
 | 
					                <div className="text-xl justify-left">TTS Message Filter</div>
 | 
				
			||||||
 | 
					                <div className="grid px-10 py-6 rounded-md bg-blue-400 max-w-sm">
 | 
				
			||||||
 | 
					                    <TTSBadgeFilterModal />
 | 
				
			||||||
 | 
					                    {/* <Button aria-label="Subscription" variant="ttsmessagefilter" className="my-2">
 | 
				
			||||||
 | 
					                        <Info className="h-4 w-4 mx-1" />
 | 
				
			||||||
 | 
					                        Subscription
 | 
				
			||||||
 | 
					                    </Button> */}
 | 
				
			||||||
 | 
					                    {/* <Button aria-label="Cheers" variant="ttsmessagefilter" className="my-2">
 | 
				
			||||||
 | 
					                        <Info className="h-4 w-4 mx-1" />
 | 
				
			||||||
 | 
					                        Cheers
 | 
				
			||||||
 | 
					                    </Button> */}
 | 
				
			||||||
 | 
					                    <Button aria-label="Username" variant="ttsmessagefilter" className="my-2">
 | 
				
			||||||
 | 
					                        <Info className="h-4 w-4 mx-1" />
 | 
				
			||||||
 | 
					                        Username
 | 
				
			||||||
 | 
					                    </Button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					export default SettingsPage;
 | 
				
			||||||
							
								
								
									
										75
									
								
								app/api/account/authorize/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								app/api/account/authorize/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					import axios from 'axios'
 | 
				
			||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GET(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const { searchParams } = new URL(req.url)
 | 
				
			||||||
 | 
					        const code = searchParams.get('code') as string
 | 
				
			||||||
 | 
					        const scope = searchParams.get('scope') as string
 | 
				
			||||||
 | 
					        const state = searchParams.get('state') as string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log("CODE:", code)
 | 
				
			||||||
 | 
					        console.log("SCOPE:", scope)
 | 
				
			||||||
 | 
					        console.log("STATE:", state)
 | 
				
			||||||
 | 
					        if (!code || !scope || !state) {
 | 
				
			||||||
 | 
					            return new NextResponse("Bad Request", { status: 400 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        console.log("VERIFY")
 | 
				
			||||||
 | 
					        // Verify state against user id in user table.
 | 
				
			||||||
 | 
					        const user = await db.user.findFirst({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            id: state
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log("USER", user)
 | 
				
			||||||
 | 
					        if (!user) {
 | 
				
			||||||
 | 
					          return new NextResponse("Bad Request", { status: 400 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log("FETCH TOKEN")
 | 
				
			||||||
 | 
					        // Post to https://id.twitch.tv/oauth2/token
 | 
				
			||||||
 | 
					        const token: { access_token:string, expires_in:number, refresh_token:string, token_type:string, scope:string[] } = (await axios.post("https://id.twitch.tv/oauth2/token", {
 | 
				
			||||||
 | 
					            client_id: process.env.TWITCH_BOT_CLIENT_ID,
 | 
				
			||||||
 | 
					            client_secret: process.env.TWITCH_BOT_CLIENT_SECRET,
 | 
				
			||||||
 | 
					            code: code,
 | 
				
			||||||
 | 
					            grant_type: "authorization_code",
 | 
				
			||||||
 | 
					            redirect_uri: "https://hermes.goblincaves.com/api/account/authorize"
 | 
				
			||||||
 | 
					        })).data
 | 
				
			||||||
 | 
					        console.log("TOKEN", token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Fetch values from token.
 | 
				
			||||||
 | 
					        const { access_token, expires_in, refresh_token, token_type } = token
 | 
				
			||||||
 | 
					        console.log("AT", access_token)
 | 
				
			||||||
 | 
					        console.log("RT", refresh_token)
 | 
				
			||||||
 | 
					        console.log("TT", token_type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!access_token || !refresh_token || token_type !== "bearer") {
 | 
				
			||||||
 | 
					            return new NextResponse("Unauthorized", { status: 401 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let info = await axios.get("https://api.twitch.tv/helix/users?login=" + user.username, {
 | 
				
			||||||
 | 
					            headers: {
 | 
				
			||||||
 | 
					                "Authorization": "Bearer " + access_token,
 | 
				
			||||||
 | 
					                "Client-Id": process.env.TWITCH_BOT_CLIENT_ID
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        console.log(info.data.data)
 | 
				
			||||||
 | 
					        const broadcasterId = info.data.data[0]['id']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await db.twitchConnection.create({
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            broadcasterId: broadcasterId,
 | 
				
			||||||
 | 
					            accessToken: access_token,
 | 
				
			||||||
 | 
					            refreshToken: refresh_token,
 | 
				
			||||||
 | 
					            userId: state
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return new NextResponse("", { status: 200 });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[ACCOUNT]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										73
									
								
								app/api/account/reauthorize/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								app/api/account/reauthorize/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					import axios from 'axios'
 | 
				
			||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { GET as authorize } from '../authorize/route'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GET(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        // Verify state against user id in user table.
 | 
				
			||||||
 | 
					        const key = await db.apiKey.findFirst({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            id: req.headers.get('x-api-key') as string
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log("API USER:", key)
 | 
				
			||||||
 | 
					        if (!key) {
 | 
				
			||||||
 | 
					            return new NextResponse("Forbidden", { status: 403 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const connection = await db.twitchConnection.findFirst({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            userId: key.userId
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        if (!connection) {
 | 
				
			||||||
 | 
					            return new NextResponse("Forbidden", { status: 403 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            const { expires_in }: { client_id:string, login:string, scopes:string[], user_id:string, expires_in:number } = (await axios.get("https://id.twitch.tv/oauth2/validate", {
 | 
				
			||||||
 | 
					              headers: {
 | 
				
			||||||
 | 
					                Authorization: 'OAuth ' + connection.accessToken
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            })).data;
 | 
				
			||||||
 | 
					            if (expires_in > 3600)
 | 
				
			||||||
 | 
					                return new NextResponse("", { status: 201 });
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.log("Oudated Twitch token.")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Post to https://id.twitch.tv/oauth2/token
 | 
				
			||||||
 | 
					        const token: { access_token:string, expires_in:number, refresh_token:string, token_type:string, scope:string[] } = (await axios.post("https://id.twitch.tv/oauth2/token", {
 | 
				
			||||||
 | 
					            client_id: process.env.TWITCH_BOT_CLIENT_ID,
 | 
				
			||||||
 | 
					            client_secret: process.env.TWITCH_BOT_CLIENT_SECRET,
 | 
				
			||||||
 | 
					            grant_type: "refresh_token",
 | 
				
			||||||
 | 
					            refresh_token: connection.refreshToken
 | 
				
			||||||
 | 
					        })).data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Fetch values from token.
 | 
				
			||||||
 | 
					        const { access_token, expires_in, refresh_token, token_type } = token
 | 
				
			||||||
 | 
					        console.log("AT", access_token)
 | 
				
			||||||
 | 
					        console.log("RT", refresh_token)
 | 
				
			||||||
 | 
					        console.log("TT", token_type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!access_token || !refresh_token || token_type !== "bearer") {
 | 
				
			||||||
 | 
					            return new NextResponse("Unauthorized", { status: 401 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await db.twitchConnection.update({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            userId: key.userId
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            accessToken: access_token
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return new NextResponse("", { status: 200 });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[ACCOUNT]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										67
									
								
								app/api/account/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								app/api/account/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { getServerSession } from "next-auth";
 | 
				
			||||||
 | 
					import { generateToken } from "../token/route";
 | 
				
			||||||
 | 
					import fetchUserUsingAPI from "@/lib/validate-api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GET(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      return NextResponse.json(await fetchUserUsingAPI(req))
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[ACCOUNT]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function POST(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const session = await getServerSession()
 | 
				
			||||||
 | 
					        const user = session?.user?.name
 | 
				
			||||||
 | 
					        if (!user) {
 | 
				
			||||||
 | 
					            return new NextResponse("Internal Error", { status: 401 })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const exist = await db.user.findFirst({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            username: user.toLowerCase() as string
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					        if (exist) {
 | 
				
			||||||
 | 
					            // const apikey = await db.apiKey.findFirst({
 | 
				
			||||||
 | 
					            //   where: {
 | 
				
			||||||
 | 
					            //     userId: user.toLowerCase() as string
 | 
				
			||||||
 | 
					            //   }
 | 
				
			||||||
 | 
					            // })
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              id: exist.id,
 | 
				
			||||||
 | 
					              username: exist.username,
 | 
				
			||||||
 | 
					              //key: apikey?.id as string
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					        const newUser = await db.user.create({
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            username: user.toLowerCase() as string,
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // const apikey = await db.apiKey.create({
 | 
				
			||||||
 | 
					        //   data: {
 | 
				
			||||||
 | 
					        //     id: generateToken(),
 | 
				
			||||||
 | 
					        //     label: "Default",
 | 
				
			||||||
 | 
					        //     userId: user.toLowerCase() as string
 | 
				
			||||||
 | 
					        //   }
 | 
				
			||||||
 | 
					        // })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return NextResponse.json({
 | 
				
			||||||
 | 
					          id: newUser.id,
 | 
				
			||||||
 | 
					          username: newUser.username,
 | 
				
			||||||
 | 
					          //key: apikey.id
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[ACCOUNT]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										40
									
								
								app/api/auth/[...nextauth]/options.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/api/auth/[...nextauth]/options.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					import type { NextAuthOptions } from "next-auth";
 | 
				
			||||||
 | 
					import TwitchProvider from "next-auth/providers/twitch";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface TwitchProfile extends Record<string, any> {
 | 
				
			||||||
 | 
					  sub: string
 | 
				
			||||||
 | 
					  preferred_username: string
 | 
				
			||||||
 | 
					  email: string
 | 
				
			||||||
 | 
					  picture: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const options: NextAuthOptions = {
 | 
				
			||||||
 | 
					  providers: [
 | 
				
			||||||
 | 
					    TwitchProvider({
 | 
				
			||||||
 | 
					      clientId: process.env.TWITCH_CLIENT_ID as string,
 | 
				
			||||||
 | 
					      clientSecret: process.env.TWITCH_CLIENT_SECRET as string,
 | 
				
			||||||
 | 
					      authorization: {
 | 
				
			||||||
 | 
					        params: {
 | 
				
			||||||
 | 
					          scope: "openid user:read:email",
 | 
				
			||||||
 | 
					          claims: {
 | 
				
			||||||
 | 
					            id_token: {
 | 
				
			||||||
 | 
					              email: null,
 | 
				
			||||||
 | 
					              picture: null,
 | 
				
			||||||
 | 
					              preferred_username: null,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      idToken: true,
 | 
				
			||||||
 | 
					      profile(profile) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          id: profile.sub,
 | 
				
			||||||
 | 
					          name: profile.preferred_username,
 | 
				
			||||||
 | 
					          email: profile.email,
 | 
				
			||||||
 | 
					          image: profile.picture,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  secret: process.env.NEXTAUTH_SECRET
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								app/api/auth/[...nextauth]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								app/api/auth/[...nextauth]/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					import NextAuth from 'next-auth'
 | 
				
			||||||
 | 
					import { options } from './options'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handler = NextAuth(options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { handler as GET, handler as POST }
 | 
				
			||||||
							
								
								
									
										23
									
								
								app/api/settings/connections/twitch/delete/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/api/settings/connections/twitch/delete/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import fetchUserUsingAPI from "@/lib/validate-api";
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function POST(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const user = await fetchUserUsingAPI(req)
 | 
				
			||||||
 | 
					        if (!user) {
 | 
				
			||||||
 | 
					            return new NextResponse("Unauthorized", { status: 401 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const connection = await db.twitchConnection.deleteMany({
 | 
				
			||||||
 | 
					            where: {
 | 
				
			||||||
 | 
					                userId: user.id
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return NextResponse.json(connection);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[CONNECTION/TWITCH]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										85
									
								
								app/api/settings/connections/twitch/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								app/api/settings/connections/twitch/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					import axios from "axios"
 | 
				
			||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import fetchUserUsingAPI from "@/lib/validate-api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GET(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const user = await fetchUserUsingAPI(req)
 | 
				
			||||||
 | 
					        if (!user) {
 | 
				
			||||||
 | 
					            console.log("TWITCH CONNECT", user)
 | 
				
			||||||
 | 
					            return new NextResponse("Unauthorized", { status: 401 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const { searchParams } = new URL(req.url)
 | 
				
			||||||
 | 
					        let userId = searchParams.get('id')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (userId == null) {
 | 
				
			||||||
 | 
					            if (user != null) {
 | 
				
			||||||
 | 
					                userId = user.id as string;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const connection = await db.twitchConnection.findFirst({
 | 
				
			||||||
 | 
					            where: {
 | 
				
			||||||
 | 
					                userId: userId as string
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return NextResponse.json(connection);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[CONNECTION/TWITCH]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function POST(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const { id, secret } = await req.json();
 | 
				
			||||||
 | 
					        const user = await fetchUserUsingAPI(req)
 | 
				
			||||||
 | 
					        console.log("userrr:", user)
 | 
				
			||||||
 | 
					        if (!user) {
 | 
				
			||||||
 | 
					            return new NextResponse("Unauthorized", { status: 401 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(id, secret)
 | 
				
			||||||
 | 
					        let response = null;
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          response = await axios.post("https://id.twitch.tv/oauth2/token", {
 | 
				
			||||||
 | 
					            client_id: id,
 | 
				
			||||||
 | 
					            client_secret: secret,
 | 
				
			||||||
 | 
					            grant_type: "client_credentials"
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          console.log(response.data)
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          console.log("[CONNECTIONS/TWITCH/TOKEN]", error);
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(user.username)
 | 
				
			||||||
 | 
					        let info = await axios.get("https://api.twitch.tv/helix/users?login=" + user.username, {
 | 
				
			||||||
 | 
					            headers: {
 | 
				
			||||||
 | 
					                "Authorization": "Bearer " + response.data['access_token'],
 | 
				
			||||||
 | 
					                "Client-Id": id
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        console.log(info.data.data)
 | 
				
			||||||
 | 
					        const broadcasterId = info.data.data[0]['id']
 | 
				
			||||||
 | 
					        const username = info.data.data[0]['login']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const connection = await db.twitchConnection.create({
 | 
				
			||||||
 | 
					            data: {
 | 
				
			||||||
 | 
					                id,
 | 
				
			||||||
 | 
					                secret,
 | 
				
			||||||
 | 
					                userId: user.id as string,
 | 
				
			||||||
 | 
					                broadcasterId,
 | 
				
			||||||
 | 
					                username
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return NextResponse.json(connection);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[CONNECTION/TWITCH]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										56
									
								
								app/api/settings/tts/filter/badges/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/api/settings/tts/filter/badges/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					import axios from "axios"
 | 
				
			||||||
 | 
					import { currentUser } from "@/lib/current-user";
 | 
				
			||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GET(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const user = await currentUser();
 | 
				
			||||||
 | 
					        if (!user) {
 | 
				
			||||||
 | 
					            return new NextResponse("Unauthorized", { status: 401 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let badges = await axios.get("https://api.twitch.tv/helix/chat/badges")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const badgesData = await db.ttsBadgeFilter.findMany({
 | 
				
			||||||
 | 
					            where: {
 | 
				
			||||||
 | 
					                data: {
 | 
				
			||||||
 | 
					                    userId: user.id
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // return NextResponse.json(badgesData);
 | 
				
			||||||
 | 
					        return new NextResponse("Bad Request", { status: 400 });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[CONNECTION/TWITCH]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function POST(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const user = await currentUser();
 | 
				
			||||||
 | 
					        const badges = await req.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log("BADGES", badges);
 | 
				
			||||||
 | 
					        if (!user) {
 | 
				
			||||||
 | 
					            return new NextResponse("Unauthorized", { status: 401 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // const badgesData = await db.tTSBadgeFilter.createMany({
 | 
				
			||||||
 | 
					        //     badges.map((badgeName, value) => {
 | 
				
			||||||
 | 
					        //         data: {
 | 
				
			||||||
 | 
					        //             userId: user.id
 | 
				
			||||||
 | 
					        //         }
 | 
				
			||||||
 | 
					        //     })
 | 
				
			||||||
 | 
					        // });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // return NextResponse.json(badgesData);
 | 
				
			||||||
 | 
					        return new NextResponse("Bad Request", { status: 400 });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[CONNECTION/TWITCH]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								app/api/token/[id]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/api/token/[id]/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import fetchUserUsingAPI from "@/lib/validate-api";
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GET(req: Request, { params } : { params: { id: string } }) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        let id = req.headers?.get('x-api-key')
 | 
				
			||||||
 | 
					        if (id == null) {
 | 
				
			||||||
 | 
					          return NextResponse.json(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const tokens = await db.apiKey.findFirst({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            id: id
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return NextResponse.json(tokens);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[TOKEN/GET]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function DELETE(req: Request, { params } : { params: { id: string } }) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const { id } = params
 | 
				
			||||||
 | 
					        const user = await fetchUserUsingAPI(req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const token = await db.apiKey.delete({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            id,
 | 
				
			||||||
 | 
					            userId: user?.id
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return NextResponse.json(token);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[TOKEN/DELETE]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										34
									
								
								app/api/token/bot/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/api/token/bot/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import fetchUserUsingAPI from "@/lib/validate-api";
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GET(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const user = await fetchUserUsingAPI(req);
 | 
				
			||||||
 | 
					        if (!user) {
 | 
				
			||||||
 | 
					          return new NextResponse("Unauthorized", { status: 401 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const api = await db.twitchConnection.findFirst({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            userId: user.id
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!api) {
 | 
				
			||||||
 | 
					          return new NextResponse("Forbidden", { status: 403 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const data = {
 | 
				
			||||||
 | 
					          client_id: process.env.TWITCH_BOT_CLIENT_ID,
 | 
				
			||||||
 | 
					          client_secret: process.env.TWITCH_BOT_CLIENT_SECRET,
 | 
				
			||||||
 | 
					          access_token: api.accessToken,
 | 
				
			||||||
 | 
					          refresh_token: api.refreshToken,
 | 
				
			||||||
 | 
					          broadcaster_id: api.broadcasterId
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return NextResponse.json(data);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[TOKENS/GET]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										63
									
								
								app/api/token/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								app/api/token/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					import fetchUserUsingAPI from "@/lib/validate-api";
 | 
				
			||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function POST(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        let { userId, label } = await req.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (userId == null) {
 | 
				
			||||||
 | 
					            const user = await fetchUserUsingAPI(req);
 | 
				
			||||||
 | 
					            if (user != null) {
 | 
				
			||||||
 | 
					                userId = user.id;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const id = generateToken()
 | 
				
			||||||
 | 
					        const token = await db.apiKey.create({
 | 
				
			||||||
 | 
					            data: {
 | 
				
			||||||
 | 
					                id,
 | 
				
			||||||
 | 
					                label,
 | 
				
			||||||
 | 
					                userId: userId as string
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return NextResponse.json(token);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[TOKEN/POST]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function DELETE(req: Request) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					      let { id } = await req.json();
 | 
				
			||||||
 | 
					      const user = await fetchUserUsingAPI(req);
 | 
				
			||||||
 | 
					      if (!id || !user) {
 | 
				
			||||||
 | 
					          return NextResponse.json(null)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const token = await db.apiKey.delete({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					              id,
 | 
				
			||||||
 | 
					              userId: user?.id
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return NextResponse.json(token);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					      console.log("[TOKEN/DELETE]", error);
 | 
				
			||||||
 | 
					      return new NextResponse("Internal Error", { status: 500});
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function generateToken() {
 | 
				
			||||||
 | 
					    var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
 | 
				
			||||||
 | 
					    var string_length = 32;
 | 
				
			||||||
 | 
					    var randomstring = '';
 | 
				
			||||||
 | 
					    for (var i = 0; i < string_length; i++) {
 | 
				
			||||||
 | 
					        var rnum = Math.floor(Math.random() * chars.length);
 | 
				
			||||||
 | 
					        randomstring += chars[rnum];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return randomstring;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								app/api/tokens/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/api/tokens/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import fetchUserUsingAPI from "@/lib/validate-api";
 | 
				
			||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GET(req: Request) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const { searchParams } = new URL(req.url)
 | 
				
			||||||
 | 
					        let userId = searchParams.get('userId')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (userId == null) {
 | 
				
			||||||
 | 
					            const user = await fetchUserUsingAPI(req);
 | 
				
			||||||
 | 
					            if (user != null) {
 | 
				
			||||||
 | 
					                userId = user.id as string;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log("TOKEN KEY:", userId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const tokens = await db.apiKey.findMany({
 | 
				
			||||||
 | 
					            where: {
 | 
				
			||||||
 | 
					                userId: userId as string
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return NextResponse.json(tokens);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log("[TOKENS/GET]", error);
 | 
				
			||||||
 | 
					        return new NextResponse("Internal Error", { status: 500});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								app/api/validate/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/api/validate/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GET(req: Request, { params } : { params: { id: string } }) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					      let id = req.headers.get('x-api-key')
 | 
				
			||||||
 | 
					      if (id == null) {
 | 
				
			||||||
 | 
					        return NextResponse.json(null);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const tokens = await db.apiKey.findFirst({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					              id: id as string
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return NextResponse.json(tokens);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					      console.log("[VALIDATE/GET]", error);
 | 
				
			||||||
 | 
					      return new NextResponse("Internal Error", { status: 500});
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								app/context/auth-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/context/auth-provider.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { SessionProvider } from 'next-auth/react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function AuthProvider({ children }: {
 | 
				
			||||||
 | 
					  children: React.ReactNode
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <SessionProvider>
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					    </SessionProvider>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2,6 +2,12 @@
 | 
				
			|||||||
@tailwind components;
 | 
					@tailwind components;
 | 
				
			||||||
@tailwind utilities;
 | 
					@tailwind utilities;
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
 | 
					html,
 | 
				
			||||||
 | 
					body,
 | 
				
			||||||
 | 
					:root {
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@layer base {
 | 
					@layer base {
 | 
				
			||||||
  :root {
 | 
					  :root {
 | 
				
			||||||
    --background: 0 0% 100%;
 | 
					    --background: 0 0% 100%;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,15 @@
 | 
				
			|||||||
import type { Metadata } from 'next'
 | 
					 | 
				
			||||||
import { Inter } from 'next/font/google'
 | 
					 | 
				
			||||||
import './globals.css'
 | 
					import './globals.css'
 | 
				
			||||||
 | 
					import type { Metadata } from 'next'
 | 
				
			||||||
 | 
					import { Open_Sans } from 'next/font/google'
 | 
				
			||||||
 | 
					import AuthProvider from './context/auth-provider'
 | 
				
			||||||
 | 
					import { ThemeProvider } from '@/components/providers/theme-provider'
 | 
				
			||||||
 | 
					import { cn } from '@/lib/utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const inter = Inter({ subsets: ['latin'] })
 | 
					const font = Open_Sans({ subsets: ['latin'] })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const metadata: Metadata = {
 | 
					export const metadata: Metadata = {
 | 
				
			||||||
  title: 'Create Next App',
 | 
					  title: 'Hermes',
 | 
				
			||||||
  description: 'Generated by create next app',
 | 
					  description: '',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function RootLayout({
 | 
					export default function RootLayout({
 | 
				
			||||||
@@ -15,8 +18,21 @@ export default function RootLayout({
 | 
				
			|||||||
  children: React.ReactNode
 | 
					  children: React.ReactNode
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <html lang="en">
 | 
					    <AuthProvider>
 | 
				
			||||||
      <body className={inter.className}>{children}</body>
 | 
					      <html lang="en">
 | 
				
			||||||
    </html>
 | 
					        <body className={cn(
 | 
				
			||||||
 | 
					          font.className,
 | 
				
			||||||
 | 
					          "bg-white dark:bg-[#000000]"
 | 
				
			||||||
 | 
					        )}>
 | 
				
			||||||
 | 
					          <ThemeProvider
 | 
				
			||||||
 | 
					          attribute="class"
 | 
				
			||||||
 | 
					          defaultTheme='dark'
 | 
				
			||||||
 | 
					          enableSystem={false}
 | 
				
			||||||
 | 
					          storageKey='global-web-theme'>
 | 
				
			||||||
 | 
					            {children}
 | 
				
			||||||
 | 
					          </ThemeProvider>
 | 
				
			||||||
 | 
					        </body>
 | 
				
			||||||
 | 
					      </html>
 | 
				
			||||||
 | 
					    </AuthProvider>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										113
									
								
								app/page.tsx
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								app/page.tsx
									
									
									
									
									
								
							@@ -1,113 +0,0 @@
 | 
				
			|||||||
import Image from 'next/image'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function Home() {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
 | 
					 | 
				
			||||||
      <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
 | 
					 | 
				
			||||||
        <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto  lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
 | 
					 | 
				
			||||||
          Get started by editing 
 | 
					 | 
				
			||||||
          <code className="font-mono font-bold">app/page.tsx</code>
 | 
					 | 
				
			||||||
        </p>
 | 
					 | 
				
			||||||
        <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
 | 
					 | 
				
			||||||
          <a
 | 
					 | 
				
			||||||
            className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
 | 
					 | 
				
			||||||
            href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 | 
					 | 
				
			||||||
            target="_blank"
 | 
					 | 
				
			||||||
            rel="noopener noreferrer"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            By{' '}
 | 
					 | 
				
			||||||
            <Image
 | 
					 | 
				
			||||||
              src="/vercel.svg"
 | 
					 | 
				
			||||||
              alt="Vercel Logo"
 | 
					 | 
				
			||||||
              className="dark:invert"
 | 
					 | 
				
			||||||
              width={100}
 | 
					 | 
				
			||||||
              height={24}
 | 
					 | 
				
			||||||
              priority
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
 | 
					 | 
				
			||||||
        <Image
 | 
					 | 
				
			||||||
          className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
 | 
					 | 
				
			||||||
          src="/next.svg"
 | 
					 | 
				
			||||||
          alt="Next.js Logo"
 | 
					 | 
				
			||||||
          width={180}
 | 
					 | 
				
			||||||
          height={37}
 | 
					 | 
				
			||||||
          priority
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 | 
					 | 
				
			||||||
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener noreferrer"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <h2 className={`mb-3 text-2xl font-semibold`}>
 | 
					 | 
				
			||||||
            Docs{' '}
 | 
					 | 
				
			||||||
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
 | 
					 | 
				
			||||||
              ->
 | 
					 | 
				
			||||||
            </span>
 | 
					 | 
				
			||||||
          </h2>
 | 
					 | 
				
			||||||
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
 | 
					 | 
				
			||||||
            Find in-depth information about Next.js features and API.
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
 | 
					 | 
				
			||||||
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener noreferrer"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <h2 className={`mb-3 text-2xl font-semibold`}>
 | 
					 | 
				
			||||||
            Learn{' '}
 | 
					 | 
				
			||||||
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
 | 
					 | 
				
			||||||
              ->
 | 
					 | 
				
			||||||
            </span>
 | 
					 | 
				
			||||||
          </h2>
 | 
					 | 
				
			||||||
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
 | 
					 | 
				
			||||||
            Learn about Next.js in an interactive course with quizzes!
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 | 
					 | 
				
			||||||
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener noreferrer"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <h2 className={`mb-3 text-2xl font-semibold`}>
 | 
					 | 
				
			||||||
            Templates{' '}
 | 
					 | 
				
			||||||
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
 | 
					 | 
				
			||||||
              ->
 | 
					 | 
				
			||||||
            </span>
 | 
					 | 
				
			||||||
          </h2>
 | 
					 | 
				
			||||||
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
 | 
					 | 
				
			||||||
            Explore starter templates for Next.js.
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 | 
					 | 
				
			||||||
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener noreferrer"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <h2 className={`mb-3 text-2xl font-semibold`}>
 | 
					 | 
				
			||||||
            Deploy{' '}
 | 
					 | 
				
			||||||
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
 | 
					 | 
				
			||||||
              ->
 | 
					 | 
				
			||||||
            </span>
 | 
					 | 
				
			||||||
          </h2>
 | 
					 | 
				
			||||||
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
 | 
					 | 
				
			||||||
            Instantly deploy your Next.js site to a shareable URL with Vercel.
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </main>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										139
									
								
								components/modals/connect-twitch-modal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								components/modals/connect-twitch-modal.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import axios from "axios"
 | 
				
			||||||
 | 
					import { db } from "@/lib/db";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import { Input } from "@/components/ui/input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as z from "zod";
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation";
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const formSchema = z.object({
 | 
				
			||||||
 | 
					  id: z.string().trim().min(12, {
 | 
				
			||||||
 | 
					    message: "Client ID should be at least 12 characters."
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  secret: z.string().trim().min(12, {
 | 
				
			||||||
 | 
					    message: "Client secret should be at least 12 characters."
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ConnectTwitchModal = () => {
 | 
				
			||||||
 | 
					  const [isMounted, setIsMounted] = useState(false);
 | 
				
			||||||
 | 
					  const [twitchError, setTwitchError] = useState("")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setIsMounted(true);
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  const router = useRouter();
 | 
				
			||||||
 | 
					  const form = useForm({
 | 
				
			||||||
 | 
					    resolver: zodResolver(formSchema),
 | 
				
			||||||
 | 
					    defaultValues: {
 | 
				
			||||||
 | 
					      id: "",
 | 
				
			||||||
 | 
					      secret: ""
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isLoading = form.formState.isSubmitting;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  if (!isMounted) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onSubmit = async (values: z.infer<typeof formSchema>) => {
 | 
				
			||||||
 | 
					    setTwitchError("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await axios.post("/api/settings/connections/twitch", values);
 | 
				
			||||||
 | 
					      console.log(response.data);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      setTwitchError("Invalid client id and client secret combination.");
 | 
				
			||||||
 | 
					      console.log("[CONNECTIONS/TWITCH/POST]", error);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    form.reset();
 | 
				
			||||||
 | 
					    router.refresh();
 | 
				
			||||||
 | 
					    window.location.reload();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog>
 | 
				
			||||||
 | 
					      <DialogTrigger>
 | 
				
			||||||
 | 
					        <Button>Connect Twitch</Button>
 | 
				
			||||||
 | 
					      </DialogTrigger>
 | 
				
			||||||
 | 
					      <DialogContent className="bg-white text-black p-0 overflow-hidden">
 | 
				
			||||||
 | 
					        <DialogHeader className="pt-8 px-6">
 | 
				
			||||||
 | 
					          <DialogTitle className="text-2xl text-center">Connect Your Twitch Account</DialogTitle>
 | 
				
			||||||
 | 
					          <DialogDescription className="text-center">Provide permission to access your twitch account & read chat.</DialogDescription>
 | 
				
			||||||
 | 
					        </DialogHeader>
 | 
				
			||||||
 | 
					        <div className={cn("hidden", twitchError.length > 0 && "block px-5 py-2 bg-[#FF0000] text-center items-center justify-center")}>
 | 
				
			||||||
 | 
					            {twitchError}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <Form {...form}>
 | 
				
			||||||
 | 
					          <form onSubmit={form.handleSubmit(onSubmit)}
 | 
				
			||||||
 | 
					          className="space-y-8">
 | 
				
			||||||
 | 
					            <div className="space-y-8 px-6">
 | 
				
			||||||
 | 
					              <FormField 
 | 
				
			||||||
 | 
					                control={form.control}
 | 
				
			||||||
 | 
					                name="id"
 | 
				
			||||||
 | 
					                render={({ field }) => (
 | 
				
			||||||
 | 
					                  <FormItem>
 | 
				
			||||||
 | 
					                    <FormLabel
 | 
				
			||||||
 | 
					                    className="uppercase text-xs font-bold">
 | 
				
			||||||
 | 
					                      Client ID
 | 
				
			||||||
 | 
					                    </FormLabel>
 | 
				
			||||||
 | 
					                    <FormControl>
 | 
				
			||||||
 | 
					                      <Input
 | 
				
			||||||
 | 
					                        disabled={isLoading}
 | 
				
			||||||
 | 
					                        className="bg-white text-black"
 | 
				
			||||||
 | 
					                        placeholder="Enter your client id"
 | 
				
			||||||
 | 
					                        {...field}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    </FormControl>
 | 
				
			||||||
 | 
					                    <FormMessage />
 | 
				
			||||||
 | 
					                  </FormItem>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div className="space-y-8 px-6">
 | 
				
			||||||
 | 
					              <FormField 
 | 
				
			||||||
 | 
					                control={form.control}
 | 
				
			||||||
 | 
					                name="secret"
 | 
				
			||||||
 | 
					                render={({ field }) => (
 | 
				
			||||||
 | 
					                  <FormItem>
 | 
				
			||||||
 | 
					                    <FormLabel
 | 
				
			||||||
 | 
					                    className="uppercase text-xs font-bold">
 | 
				
			||||||
 | 
					                      Client secret
 | 
				
			||||||
 | 
					                    </FormLabel>
 | 
				
			||||||
 | 
					                    <FormControl>
 | 
				
			||||||
 | 
					                      <Input
 | 
				
			||||||
 | 
					                        disabled={isLoading}
 | 
				
			||||||
 | 
					                        className="bg-white text-black"
 | 
				
			||||||
 | 
					                        placeholder="Enter your client secret"
 | 
				
			||||||
 | 
					                        {...field}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    </FormControl>
 | 
				
			||||||
 | 
					                    <FormMessage />
 | 
				
			||||||
 | 
					                  </FormItem>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <DialogFooter className="bg-gray-100 px-6 py-4">
 | 
				
			||||||
 | 
					                <Button disabled={isLoading}>Connect</Button>
 | 
				
			||||||
 | 
					              </DialogFooter>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </form>
 | 
				
			||||||
 | 
					        </Form>
 | 
				
			||||||
 | 
					      </DialogContent>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										109
									
								
								components/modals/tts-badge-filter-modal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								components/modals/tts-badge-filter-modal.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import axios from "axios"
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import { Input } from "@/components/ui/input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as z from "zod";
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation";
 | 
				
			||||||
 | 
					import { Info } from "lucide-react";
 | 
				
			||||||
 | 
					import { Toggle } from "../ui/toggle";
 | 
				
			||||||
 | 
					import { TtsBadgeFilter, TwitchConnection } from "@prisma/client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const formSchema = z.object({
 | 
				
			||||||
 | 
					  whitelist: z.string().trim().array(),
 | 
				
			||||||
 | 
					  blacklist: z.string().trim().array()
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const TTSBadgeFilterModal = () => {
 | 
				
			||||||
 | 
					  const [isMounted, setIsMounted] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setIsMounted(true);
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  const router = useRouter();
 | 
				
			||||||
 | 
					  const form = useForm({
 | 
				
			||||||
 | 
					    resolver: zodResolver(formSchema),
 | 
				
			||||||
 | 
					    defaultValues: {
 | 
				
			||||||
 | 
					      whitelist: z.string().trim().array(),
 | 
				
			||||||
 | 
					      blacklist: z.string().trim().array()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isLoading = form.formState.isSubmitting;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  const onSubmit = async (values: z.infer<typeof formSchema>) => {
 | 
				
			||||||
 | 
					    let response = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // try {
 | 
				
			||||||
 | 
					    //   response = await axios.post("/api/settings/tts/filter/badges", values);
 | 
				
			||||||
 | 
					    // } catch (error) {
 | 
				
			||||||
 | 
					    //   console.log("[CONNECTIONS/TWITCH/POST]", error);
 | 
				
			||||||
 | 
					    //   return;
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    form.reset();
 | 
				
			||||||
 | 
					    router.refresh();
 | 
				
			||||||
 | 
					    window.location.reload();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!isMounted) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const badges: TtsBadgeFilter[] = [] //(await axios.get("/api/settings/tts/filter/badges")).data
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog>
 | 
				
			||||||
 | 
					      <DialogTrigger>
 | 
				
			||||||
 | 
					        <Button aria-label="Badge" variant="ttsmessagefilter" className="my-2">
 | 
				
			||||||
 | 
					          <Info className="h-4 w-4 mx-1" />
 | 
				
			||||||
 | 
					          Badge
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </DialogTrigger>
 | 
				
			||||||
 | 
					      <DialogContent className="bg-white text-black p-0 overflow-hidden">
 | 
				
			||||||
 | 
					        <DialogHeader className="pt-8 px-6">
 | 
				
			||||||
 | 
					          <DialogTitle className="text-2xl text-center">TTS Badge Filter</DialogTitle>
 | 
				
			||||||
 | 
					          <DialogDescription className="text-center">Limit messages spoken by TTS by badges.</DialogDescription>
 | 
				
			||||||
 | 
					        </DialogHeader>
 | 
				
			||||||
 | 
					        <Form {...form}>
 | 
				
			||||||
 | 
					          <form onSubmit={form.handleSubmit(onSubmit)}
 | 
				
			||||||
 | 
					          className="space-y-10">
 | 
				
			||||||
 | 
					            {badges.map((badge) => (
 | 
				
			||||||
 | 
					              <div className="space-y-8 px-6">
 | 
				
			||||||
 | 
					                <FormField
 | 
				
			||||||
 | 
					                  key={badge.badgeId}
 | 
				
			||||||
 | 
					                  control={form.control}
 | 
				
			||||||
 | 
					                  name={badge.badgeId}
 | 
				
			||||||
 | 
					                  render={({ field }) => {
 | 
				
			||||||
 | 
					                      return (
 | 
				
			||||||
 | 
					                        <FormItem>
 | 
				
			||||||
 | 
					                          <Toggle
 | 
				
			||||||
 | 
					                            aria-label="Badge"
 | 
				
			||||||
 | 
					                            variant="simple"
 | 
				
			||||||
 | 
					                            disabled={isLoading}
 | 
				
			||||||
 | 
					                            className="my-2"
 | 
				
			||||||
 | 
					                            {...field}>
 | 
				
			||||||
 | 
					                            {badge.badgeId}
 | 
				
			||||||
 | 
					                          </Toggle>
 | 
				
			||||||
 | 
					                        </FormItem>
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					            <DialogFooter className="bg-gray-100 px-6 py-4">
 | 
				
			||||||
 | 
					              <Button disabled={isLoading}>Save</Button>
 | 
				
			||||||
 | 
					            </DialogFooter>
 | 
				
			||||||
 | 
					          </form>
 | 
				
			||||||
 | 
					        </Form>
 | 
				
			||||||
 | 
					      </DialogContent>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										0
									
								
								components/modals/tts-cheers-filter-modal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								components/modals/tts-cheers-filter-modal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								components/modals/tts-subscription-filter-modal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								components/modals/tts-subscription-filter-modal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								components/modals/tts-username-filter-modal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								components/modals/tts-username-filter-modal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										40
									
								
								components/mode-toggle.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								components/mode-toggle.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import { Moon, Sun } from "lucide-react"
 | 
				
			||||||
 | 
					import { useTheme } from "next-themes"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DropdownMenu,
 | 
				
			||||||
 | 
					  DropdownMenuContent,
 | 
				
			||||||
 | 
					  DropdownMenuItem,
 | 
				
			||||||
 | 
					  DropdownMenuTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/dropdown-menu"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ModeToggle() {
 | 
				
			||||||
 | 
					  const { setTheme } = useTheme()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <DropdownMenu>
 | 
				
			||||||
 | 
					      <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					        <Button variant="outline" size="icon">
 | 
				
			||||||
 | 
					          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
 | 
				
			||||||
 | 
					          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
 | 
				
			||||||
 | 
					          <span className="sr-only">Toggle theme</span>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					      <DropdownMenuContent align="end">
 | 
				
			||||||
 | 
					        <DropdownMenuItem onClick={() => setTheme("light")}>
 | 
				
			||||||
 | 
					          Light
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					        <DropdownMenuItem onClick={() => setTheme("dark")}>
 | 
				
			||||||
 | 
					          Dark
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					        <DropdownMenuItem onClick={() => setTheme("system")}>
 | 
				
			||||||
 | 
					          System
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					      </DropdownMenuContent>
 | 
				
			||||||
 | 
					    </DropdownMenu>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								components/providers/theme-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								components/providers/theme-provider.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import { ThemeProvider as NextThemesProvider } from "next-themes"
 | 
				
			||||||
 | 
					import { type ThemeProviderProps } from "next-themes/dist/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
 | 
				
			||||||
 | 
					  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								components/ui/avatar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								components/ui/avatar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as AvatarPrimitive from "@radix-ui/react-avatar"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Avatar = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof AvatarPrimitive.Root>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <AvatarPrimitive.Root
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					Avatar.displayName = AvatarPrimitive.Root.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AvatarImage = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof AvatarPrimitive.Image>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <AvatarPrimitive.Image
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("aspect-square h-full w-full", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					AvatarImage.displayName = AvatarPrimitive.Image.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AvatarFallback = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof AvatarPrimitive.Fallback>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <AvatarPrimitive.Fallback
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex h-full w-full items-center justify-center rounded-full bg-muted",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Avatar, AvatarImage, AvatarFallback }
 | 
				
			||||||
							
								
								
									
										122
									
								
								components/ui/dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								components/ui/dialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,122 @@
 | 
				
			|||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as DialogPrimitive from "@radix-ui/react-dialog"
 | 
				
			||||||
 | 
					import { X } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Dialog = DialogPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogTrigger = DialogPrimitive.Trigger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogPortal = DialogPrimitive.Portal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogClose = DialogPrimitive.Close
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogOverlay = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Overlay>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPrimitive.Overlay
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Content>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
 | 
				
			||||||
 | 
					>(({ className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPortal>
 | 
				
			||||||
 | 
					    <DialogOverlay />
 | 
				
			||||||
 | 
					    <DialogPrimitive.Content
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
 | 
				
			||||||
 | 
					        className
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
 | 
				
			||||||
 | 
					        <X className="h-4 w-4" />
 | 
				
			||||||
 | 
					        <span className="sr-only">Close</span>
 | 
				
			||||||
 | 
					      </DialogPrimitive.Close>
 | 
				
			||||||
 | 
					    </DialogPrimitive.Content>
 | 
				
			||||||
 | 
					  </DialogPortal>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogContent.displayName = DialogPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogHeader = ({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex flex-col space-y-1.5 text-center sm:text-left",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					DialogHeader.displayName = "DialogHeader"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogFooter = ({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					DialogFooter.displayName = "DialogFooter"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogTitle = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Title>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPrimitive.Title
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "text-lg font-semibold leading-none tracking-tight",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogTitle.displayName = DialogPrimitive.Title.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogDescription = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Description>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPrimitive.Description
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-sm text-muted-foreground", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogDescription.displayName = DialogPrimitive.Description.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  Dialog,
 | 
				
			||||||
 | 
					  DialogPortal,
 | 
				
			||||||
 | 
					  DialogOverlay,
 | 
				
			||||||
 | 
					  DialogClose,
 | 
				
			||||||
 | 
					  DialogTrigger,
 | 
				
			||||||
 | 
					  DialogContent,
 | 
				
			||||||
 | 
					  DialogHeader,
 | 
				
			||||||
 | 
					  DialogFooter,
 | 
				
			||||||
 | 
					  DialogTitle,
 | 
				
			||||||
 | 
					  DialogDescription,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										100
									
								
								lib/audio/audio.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								lib/audio/audio.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					import { createPubSub } from '../pubsub';
 | 
				
			||||||
 | 
					import { AudioState } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createAudio = () => {
 | 
				
			||||||
 | 
					  const pubsub = createPubSub();
 | 
				
			||||||
 | 
					  const element = document.createElement('video');
 | 
				
			||||||
 | 
					  let currentTime = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let state: AudioState = {
 | 
				
			||||||
 | 
					    duration: 0,
 | 
				
			||||||
 | 
					    playing: false,
 | 
				
			||||||
 | 
					    volume: 0,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const setState = (value: Partial<AudioState>) => {
 | 
				
			||||||
 | 
					    state = { ...state, ...value };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pubsub.publish('change', state);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const setup = () => {
 | 
				
			||||||
 | 
					    element.addEventListener('durationchange', () =>
 | 
				
			||||||
 | 
					      setState({ duration: element.duration }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    element.addEventListener('playing', () => setState({ playing: true }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    element.addEventListener('pause', () => setState({ playing: false }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    element.addEventListener('timeupdate', () => {
 | 
				
			||||||
 | 
					      const newCurrentTime = Math.round(element.currentTime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (currentTime !== newCurrentTime) {
 | 
				
			||||||
 | 
					        currentTime = newCurrentTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pubsub.publish('change-current-time', currentTime);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    element.addEventListener('volumechange', () =>
 | 
				
			||||||
 | 
					      setState({ volume: element.volume }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState({ volume: element.volume });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setup();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    seek(seconds: number) {
 | 
				
			||||||
 | 
					      element.currentTime = seconds;
 | 
				
			||||||
 | 
					      currentTime = seconds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      pubsub.publish('change-current-time', currentTime);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getElement() {
 | 
				
			||||||
 | 
					      return element;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getState() {
 | 
				
			||||||
 | 
					      return state;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getCurrentTime() {
 | 
				
			||||||
 | 
					      return currentTime;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    play() {
 | 
				
			||||||
 | 
					      element.play();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pause() {
 | 
				
			||||||
 | 
					      element.pause();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    volume(value: number) {
 | 
				
			||||||
 | 
					      element.volume = value;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setUrl(url: string) {
 | 
				
			||||||
 | 
					      element.setAttribute('src', url);
 | 
				
			||||||
 | 
					      setState({ playing: false });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    subscribe(listener: (newState: AudioState) => void) {
 | 
				
			||||||
 | 
					      return pubsub.subscribe('change', listener);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onChangeCurrentTime(listener: (newCurrentTime: number) => void) {
 | 
				
			||||||
 | 
					      return pubsub.subscribe('change-current-time', listener);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onEnded(listener: () => void) {
 | 
				
			||||||
 | 
					      element.addEventListener('ended', listener);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return () => element.removeEventListener('ended', listener);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										27
									
								
								lib/audio/hooks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/audio/hooks.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import { useState, useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import player from './player';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const usePlayerState = () => {
 | 
				
			||||||
 | 
					  const [state, setState] = useState(player.getState());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const unsubscribe = player.subscribe(setState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return unsubscribe;
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return state;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useCurrentTime = () => {
 | 
				
			||||||
 | 
					  const [currentTime, setCurrentTime] = useState(player.getCurrentTime());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const unsubscribe = player.onChangeCurrentTime(setCurrentTime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return unsubscribe;
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return currentTime;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										106
									
								
								lib/audio/player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								lib/audio/player.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					import { createPubSub } from '../pubsub';
 | 
				
			||||||
 | 
					import { createAudio } from './audio';
 | 
				
			||||||
 | 
					import { State, Track } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createPlayer = () => {
 | 
				
			||||||
 | 
					  const pubsub = createPubSub();
 | 
				
			||||||
 | 
					  const audio = createAudio();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let state: State = {
 | 
				
			||||||
 | 
					    ...audio.getState(),
 | 
				
			||||||
 | 
					    tracks: [],
 | 
				
			||||||
 | 
					    currentTrackIndex: null,
 | 
				
			||||||
 | 
					    currentTrack: null,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const setState = (value: Partial<State>) => {
 | 
				
			||||||
 | 
					    state = { ...state, ...value };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pubsub.publish('change', state);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  audio.subscribe(setState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const changeTrack = () => {
 | 
				
			||||||
 | 
					    const track = state.currentTrack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (track) {
 | 
				
			||||||
 | 
					      audio.setUrl(track.url);
 | 
				
			||||||
 | 
					      audio.play();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const next = () => {
 | 
				
			||||||
 | 
					    if (state.currentTrackIndex === null) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const lastIndex = state.tracks.length - 1;
 | 
				
			||||||
 | 
					    const newIndex = state.currentTrackIndex + 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (newIndex <= lastIndex) {
 | 
				
			||||||
 | 
					      setState({
 | 
				
			||||||
 | 
					        currentTrackIndex: newIndex,
 | 
				
			||||||
 | 
					        currentTrack: state.tracks[newIndex],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      changeTrack();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  audio.onEnded(next);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    play: audio.play,
 | 
				
			||||||
 | 
					    pause: audio.pause,
 | 
				
			||||||
 | 
					    seek: audio.seek,
 | 
				
			||||||
 | 
					    volume: audio.volume,
 | 
				
			||||||
 | 
					    getCurrentTime: audio.getCurrentTime,
 | 
				
			||||||
 | 
					    getElement: audio.getElement,
 | 
				
			||||||
 | 
					    onChangeCurrentTime: audio.onChangeCurrentTime,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getState() {
 | 
				
			||||||
 | 
					      return state;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setQueue(tracks: Track[]) {
 | 
				
			||||||
 | 
					      setState({ tracks });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    playTrack(trackIndex: number) {
 | 
				
			||||||
 | 
					      setState({
 | 
				
			||||||
 | 
					        currentTrackIndex: trackIndex,
 | 
				
			||||||
 | 
					        currentTrack: state.tracks[trackIndex],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      changeTrack();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    next,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    prev() {
 | 
				
			||||||
 | 
					      if (state.currentTrackIndex === null) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const newIndex = state.currentTrackIndex - 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (newIndex >= 0) {
 | 
				
			||||||
 | 
					        setState({
 | 
				
			||||||
 | 
					          currentTrack: state.tracks[newIndex],
 | 
				
			||||||
 | 
					          currentTrackIndex: newIndex,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        changeTrack();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    subscribe(listener: (newState: State) => void) {
 | 
				
			||||||
 | 
					      return pubsub.subscribe('change', listener);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const player = createPlayer();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default player;
 | 
				
			||||||
							
								
								
									
										17
									
								
								lib/audio/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/audio/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					export type AudioState = {
 | 
				
			||||||
 | 
					    duration: number;
 | 
				
			||||||
 | 
					    playing: boolean;
 | 
				
			||||||
 | 
					    volume: number;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  export type Track = {
 | 
				
			||||||
 | 
					    url: string;
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  export type State = AudioState & {
 | 
				
			||||||
 | 
					    tracks: Track[];
 | 
				
			||||||
 | 
					    currentTrack: Track | null;
 | 
				
			||||||
 | 
					    currentTrackIndex: number | null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
							
								
								
									
										18
									
								
								lib/current-user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								lib/current-user.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import { db } from "@/lib/db"
 | 
				
			||||||
 | 
					import { useSession } from "next-auth/react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const currentUser = async() => {
 | 
				
			||||||
 | 
					    const { data: session, status } = useSession()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (status !== "authenticated") {
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const user = await db.user.findUnique({
 | 
				
			||||||
 | 
					        where: {
 | 
				
			||||||
 | 
					            id: session?.user?.name as string
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return user;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								lib/db.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/db.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					import { PrismaClient } from "@prisma/client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					    var prisma: PrismaClient | undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const db = globalThis.prisma || new PrismaClient();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (process.env.NODE_ENV !== "production") globalThis.prisma = db
 | 
				
			||||||
							
								
								
									
										50
									
								
								lib/pubsub.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								lib/pubsub.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					type Listener = (value: any) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Topics = {
 | 
				
			||||||
 | 
					  [name: string]: Listener[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createPubSub = () => {
 | 
				
			||||||
 | 
					  let topics: Topics = {};
 | 
				
			||||||
 | 
					  let destroyed = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getTopic = (name: string) => {
 | 
				
			||||||
 | 
					    if (!topics[name]) {
 | 
				
			||||||
 | 
					      topics[name] = [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return topics[name];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    subscribe(topic: string, fn: Listener) {
 | 
				
			||||||
 | 
					      const listeners = getTopic(topic);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      listeners.push(fn);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const unsubscribe = () => {
 | 
				
			||||||
 | 
					        const index = listeners.indexOf(fn);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        listeners.splice(index, 1);
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return unsubscribe;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    publish(topic: string, value: any) {
 | 
				
			||||||
 | 
					      const listeners = getTopic(topic);
 | 
				
			||||||
 | 
					      const currentListeners = listeners.slice();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      currentListeners.forEach((listener) => {
 | 
				
			||||||
 | 
					        if (!destroyed) {
 | 
				
			||||||
 | 
					          listener(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    destroy() {
 | 
				
			||||||
 | 
					      topics = {};
 | 
				
			||||||
 | 
					      destroyed = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										44
									
								
								lib/validate-api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								lib/validate-api.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					import { getServerSession } from "next-auth";
 | 
				
			||||||
 | 
					import { db } from "./db";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function fetchUserUsingAPI(req: Request) {
 | 
				
			||||||
 | 
					    const session = await getServerSession()
 | 
				
			||||||
 | 
					    console.log("server session:", session)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (session) {
 | 
				
			||||||
 | 
					        const user = await db.user.findFirst({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            username: session.user?.name?.toLowerCase() as string
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          id: user?.id,
 | 
				
			||||||
 | 
					          username: user?.username
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const token = req.headers?.get('x-api-key')
 | 
				
			||||||
 | 
					    console.log("x-api-key:", token)
 | 
				
			||||||
 | 
					    if (token === null || token === undefined)
 | 
				
			||||||
 | 
					        return null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const key = await db.apiKey.findFirst({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        id: token as string
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const user = await db.user.findFirst({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        id: key?.userId
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("user:", user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: user?.id,
 | 
				
			||||||
 | 
					      username: user?.username
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										86
									
								
								middleware.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								middleware.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
				
			|||||||
 | 
					// import { authMiddleware } from "@clerk/nextjs";
 | 
				
			||||||
 | 
					// import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					// // This example protects all routes including api/trpc routes
 | 
				
			||||||
 | 
					// // Please edit this to allow other routes to be public as needed.
 | 
				
			||||||
 | 
					// // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your middleware
 | 
				
			||||||
 | 
					// export default authMiddleware({
 | 
				
			||||||
 | 
					//   publicRoutes: ["/api/:path*"],
 | 
				
			||||||
 | 
					//   ignoredRoutes: ["/api/validate/:path*"],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//   beforeAuth: async (req) => {
 | 
				
			||||||
 | 
					//     // if (req.url.startsWith("https://localhost:3000/api") /*&& !req.url.startsWith("https://localhost:3000/api/validate/")*/) {
 | 
				
			||||||
 | 
					//     //   const apiKey = req.headers.get("x-api-key") as string
 | 
				
			||||||
 | 
					//     //   let api = null
 | 
				
			||||||
 | 
					//     //   if (apiKey != null) {
 | 
				
			||||||
 | 
					//     //     console.log("API KEY:", apiKey)
 | 
				
			||||||
 | 
					//     //     api = await fetch("http://localhost:3000/api/validate")
 | 
				
			||||||
 | 
					//     //   }
 | 
				
			||||||
 | 
					//     //   if (api == null) {
 | 
				
			||||||
 | 
					//     //     console.log("Invalid API key attempted")
 | 
				
			||||||
 | 
					//     //     return NextResponse.rewrite(
 | 
				
			||||||
 | 
					//     //       `${req.nextUrl.protocol}//${req.nextUrl.host}`,
 | 
				
			||||||
 | 
					//     //       {
 | 
				
			||||||
 | 
					//     //         status: 401,
 | 
				
			||||||
 | 
					//     //         headers: {
 | 
				
			||||||
 | 
					//     //           "WWW-Authenticate": 'Basic realm="Secure Area"',
 | 
				
			||||||
 | 
					//     //         },
 | 
				
			||||||
 | 
					//     //       }
 | 
				
			||||||
 | 
					//     //     );
 | 
				
			||||||
 | 
					//     //   }
 | 
				
			||||||
 | 
					//     // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//     return NextResponse.next();
 | 
				
			||||||
 | 
					//   }
 | 
				
			||||||
 | 
					// });
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					// export const config = {
 | 
				
			||||||
 | 
					//   matcher: ["/((?!.*\\..*|_next).*)", "/", "/(trpc)(.*)"],
 | 
				
			||||||
 | 
					// };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { redirect } from 'next/navigation';
 | 
				
			||||||
 | 
					import { withAuth } from 'next-auth/middleware';
 | 
				
			||||||
 | 
					import { getServerSession } from "next-auth";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default withAuth(
 | 
				
			||||||
 | 
					  async function middleware(req) {
 | 
				
			||||||
 | 
					    // if (!req.url.startsWith("https://hermes.goblincaves.com")) {
 | 
				
			||||||
 | 
					    //   return redirect("https://hermes.goblincaves.com")
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //console.log(req.nextauth)
 | 
				
			||||||
 | 
					    //console.log(req.nextauth.token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // if (req.nextUrl.pathname.startsWith("/api/auth")) {
 | 
				
			||||||
 | 
					    //   //console.log("Auth API reached")
 | 
				
			||||||
 | 
					    // } else if (req.nextUrl.pathname.startsWith("/api")) {
 | 
				
			||||||
 | 
					    //   //console.log("API reached")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //   const apiKey = req.headers.get("x-api-key") as string
 | 
				
			||||||
 | 
					    //   let api = null
 | 
				
			||||||
 | 
					    //   if (apiKey != null) {
 | 
				
			||||||
 | 
					    //     //console.log("API KEY:", apiKey)
 | 
				
			||||||
 | 
					    //     api = await fetch("http://localhost:3000/api/validate")
 | 
				
			||||||
 | 
					    //   }
 | 
				
			||||||
 | 
					    //   if (api == null || (await api.text()) == "null") {
 | 
				
			||||||
 | 
					    //     //console.log("Invalid API key attempted")
 | 
				
			||||||
 | 
					    //     return NextResponse.rewrite(
 | 
				
			||||||
 | 
					    //       `${req.nextUrl.protocol}//${req.nextUrl.host}`,
 | 
				
			||||||
 | 
					    //       {
 | 
				
			||||||
 | 
					    //         status: 401,
 | 
				
			||||||
 | 
					    //         headers: {
 | 
				
			||||||
 | 
					    //           "WWW-Authenticate": 'Basic realm="Secure Area"',
 | 
				
			||||||
 | 
					    //         },
 | 
				
			||||||
 | 
					    //       }
 | 
				
			||||||
 | 
					    //     );
 | 
				
			||||||
 | 
					    //   }
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					  callbacks: {
 | 
				
			||||||
 | 
					    authorized: async ({ req, token }) =>
 | 
				
			||||||
 | 
					      req.nextUrl.pathname?.slice(0, 4) === '/api' ||
 | 
				
			||||||
 | 
					      !!token
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										5848
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5848
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										49
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								package.json
									
									
									
									
									
								
							@@ -1,49 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "name": "hermes",
 | 
					 | 
				
			||||||
  "version": "0.1.0",
 | 
					 | 
				
			||||||
  "private": true,
 | 
					 | 
				
			||||||
  "scripts": {
 | 
					 | 
				
			||||||
    "dev": "next dev",
 | 
					 | 
				
			||||||
    "build": "next build",
 | 
					 | 
				
			||||||
    "start": "next start",
 | 
					 | 
				
			||||||
    "lint": "next lint"
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "dependencies": {
 | 
					 | 
				
			||||||
    "@hookform/resolvers": "^3.3.3",
 | 
					 | 
				
			||||||
    "@radix-ui/react-checkbox": "^1.0.4",
 | 
					 | 
				
			||||||
    "@radix-ui/react-dropdown-menu": "^2.0.6",
 | 
					 | 
				
			||||||
    "@radix-ui/react-hover-card": "^1.0.7",
 | 
					 | 
				
			||||||
    "@radix-ui/react-label": "^2.0.2",
 | 
					 | 
				
			||||||
    "@radix-ui/react-progress": "^1.0.3",
 | 
					 | 
				
			||||||
    "@radix-ui/react-radio-group": "^1.1.3",
 | 
					 | 
				
			||||||
    "@radix-ui/react-scroll-area": "^1.0.5",
 | 
					 | 
				
			||||||
    "@radix-ui/react-separator": "^1.0.3",
 | 
					 | 
				
			||||||
    "@radix-ui/react-slider": "^1.1.2",
 | 
					 | 
				
			||||||
    "@radix-ui/react-slot": "^1.0.2",
 | 
					 | 
				
			||||||
    "@radix-ui/react-switch": "^1.0.3",
 | 
					 | 
				
			||||||
    "@radix-ui/react-toast": "^1.1.5",
 | 
					 | 
				
			||||||
    "@radix-ui/react-toggle": "^1.0.3",
 | 
					 | 
				
			||||||
    "@radix-ui/react-toggle-group": "^1.0.4",
 | 
					 | 
				
			||||||
    "class-variance-authority": "^0.7.0",
 | 
					 | 
				
			||||||
    "clsx": "^2.1.0",
 | 
					 | 
				
			||||||
    "lucide-react": "^0.303.0",
 | 
					 | 
				
			||||||
    "next": "14.0.4",
 | 
					 | 
				
			||||||
    "react": "^18",
 | 
					 | 
				
			||||||
    "react-dom": "^18",
 | 
					 | 
				
			||||||
    "react-hook-form": "^7.49.2",
 | 
					 | 
				
			||||||
    "tailwind-merge": "^2.2.0",
 | 
					 | 
				
			||||||
    "tailwindcss-animate": "^1.0.7",
 | 
					 | 
				
			||||||
    "zod": "^3.22.4"
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "devDependencies": {
 | 
					 | 
				
			||||||
    "@types/node": "^20",
 | 
					 | 
				
			||||||
    "@types/react": "^18",
 | 
					 | 
				
			||||||
    "@types/react-dom": "^18",
 | 
					 | 
				
			||||||
    "autoprefixer": "^10.0.1",
 | 
					 | 
				
			||||||
    "eslint": "^8",
 | 
					 | 
				
			||||||
    "eslint-config-next": "14.0.4",
 | 
					 | 
				
			||||||
    "postcss": "^8",
 | 
					 | 
				
			||||||
    "tailwindcss": "^3.3.0",
 | 
					 | 
				
			||||||
    "typescript": "^5"
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										110
									
								
								prisma/schema.prisma
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								prisma/schema.prisma
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
				
			|||||||
 | 
					generator client {
 | 
				
			||||||
 | 
					  provider = "prisma-client-js"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					datasource db {
 | 
				
			||||||
 | 
					  provider = "mysql"
 | 
				
			||||||
 | 
					  url = env("DATABASE_URL")
 | 
				
			||||||
 | 
					  relationMode = "prisma"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model User {
 | 
				
			||||||
 | 
					  id String @id @default(uuid())
 | 
				
			||||||
 | 
					  username String @unique
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  apiKeys ApiKey[]
 | 
				
			||||||
 | 
					  twitchConnections TwitchConnection[]
 | 
				
			||||||
 | 
					  createdProfiles TtsProfile[]
 | 
				
			||||||
 | 
					  profileStatus TtsProfileStatus[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  createdAt DateTime @default(now())
 | 
				
			||||||
 | 
					  updatedAt DateTime @updatedAt
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model ApiKey {
 | 
				
			||||||
 | 
					  id String @id @default(uuid())
 | 
				
			||||||
 | 
					  label String
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  userId String
 | 
				
			||||||
 | 
					  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([userId])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model TwitchConnection {
 | 
				
			||||||
 | 
					  broadcasterId String @unique
 | 
				
			||||||
 | 
					  accessToken String
 | 
				
			||||||
 | 
					  refreshToken String
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  userId String @id
 | 
				
			||||||
 | 
					  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([userId])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model TtsProfile {
 | 
				
			||||||
 | 
					  id String @id @default(uuid())
 | 
				
			||||||
 | 
					  name String
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  createdAt DateTime @default(now())
 | 
				
			||||||
 | 
					  updatedAt DateTime @updatedAt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  statuses TtsProfileStatus[]
 | 
				
			||||||
 | 
					  badgeFilters TtsBadgeFilter[]
 | 
				
			||||||
 | 
					  usernameFilters TtsUsernameFilter[]
 | 
				
			||||||
 | 
					  wordFilters TtsWordReplacementFilter[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  creatorId String
 | 
				
			||||||
 | 
					  creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([creatorId])
 | 
				
			||||||
 | 
					  @@unique([creatorId, name])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model TtsProfileStatus {
 | 
				
			||||||
 | 
					  id String @id @default(uuid())
 | 
				
			||||||
 | 
					  name String
 | 
				
			||||||
 | 
					  enabled Boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  userId String
 | 
				
			||||||
 | 
					  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  profileId String
 | 
				
			||||||
 | 
					  profile TtsProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([userId])
 | 
				
			||||||
 | 
					  @@index([profileId])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model TtsBadgeFilter {
 | 
				
			||||||
 | 
					  badgeId String
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  profileId String
 | 
				
			||||||
 | 
					  profile TtsProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([profileId])
 | 
				
			||||||
 | 
					  @@id([profileId, badgeId])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model TtsUsernameFilter {
 | 
				
			||||||
 | 
					  username String
 | 
				
			||||||
 | 
					  white Boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  profileId String
 | 
				
			||||||
 | 
					  profile TtsProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([profileId])
 | 
				
			||||||
 | 
					  @@id([profileId, username])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model TtsWordReplacementFilter {
 | 
				
			||||||
 | 
					  word String
 | 
				
			||||||
 | 
					  replace String
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  profileId String
 | 
				
			||||||
 | 
					  profile TtsProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([profileId])
 | 
				
			||||||
 | 
					  @@id([profileId, word])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Reference in New Issue
	
	Block a user