feat: add initial implementation for fetching avatar data from API
- Created a new sync.ts file to handle API requests. - Defined interfaces for AvatarApiResponse, Data, and Avatar. - Implemented a main function to fetch avatar data from the specified endpoint.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const revalidate = 900;
|
||||
|
||||
export type YoutubeLiveInfo = {
|
||||
url: string;
|
||||
thumbnails: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
} | "none";
|
||||
|
||||
type APISearchResource = {
|
||||
"kind": "youtube#searchResult",
|
||||
"etag": string,
|
||||
"id": {
|
||||
"kind": string,
|
||||
"videoId": string,
|
||||
"channelId": string,
|
||||
"playlistId": string
|
||||
},
|
||||
"snippet": {
|
||||
"publishedAt": string,
|
||||
"channelId": string,
|
||||
"title": string,
|
||||
"description": string,
|
||||
"thumbnails": {
|
||||
[key: string]: {
|
||||
"url": string,
|
||||
"width": number,
|
||||
"height": number
|
||||
}
|
||||
},
|
||||
"channelTitle": string,
|
||||
"liveBroadcastContent": string
|
||||
}
|
||||
};
|
||||
|
||||
type APISearchResponse = {
|
||||
"kind": "youtube#searchListResponse",
|
||||
"etag": string,
|
||||
"nextPageToken": string,
|
||||
"prevPageToken": string,
|
||||
"regionCode": string,
|
||||
"pageInfo": {
|
||||
"totalResults": number,
|
||||
"resultsPerPage": number
|
||||
},
|
||||
"items": APISearchResource[],
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
const channelId = process.env.YOUTUBE_CHANNEL_ID;
|
||||
const apiKey = process.env.YOUTUBE_API_KEY;
|
||||
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${channelId}&eventType=live&type=video&maxResults=1&key=${apiKey}`,
|
||||
{
|
||||
next: {
|
||||
revalidate: 900, // refresh every 15 minutes
|
||||
},
|
||||
cache: "force-cache",
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(`YouTube API error: ${response.statusText}`);
|
||||
|
||||
const data: APISearchResponse = await response.json();
|
||||
if (data.items.length <= 0)
|
||||
return NextResponse.json<YoutubeLiveInfo>("none");
|
||||
const live = data.items[0];
|
||||
return NextResponse.json<YoutubeLiveInfo>({
|
||||
url: `https://www.youtube.com/watch?v=${live.id.videoId}`,
|
||||
thumbnails: live.snippet.thumbnails[Object.keys(live.snippet.thumbnails)[0]]
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import Avatar from "@/components/avatar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
|
||||
export function CharacterChooser() {
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">UID</Label>
|
||||
<Input id="password" type="number" required placeholder="887654321" />
|
||||
</div>
|
||||
<ScrollArea>
|
||||
<div className="flex gap-2">
|
||||
<Avatar
|
||||
char={{
|
||||
name: "Traveler",
|
||||
stars: 5,
|
||||
vision: "anemo",
|
||||
weapon: "sword",
|
||||
image: "1234567890",
|
||||
version: "5.7",
|
||||
}}
|
||||
scale={0.6}
|
||||
/>
|
||||
<Avatar
|
||||
char={{
|
||||
name: "Traveler",
|
||||
stars: 5,
|
||||
vision: "anemo",
|
||||
weapon: "sword",
|
||||
image: "1234567890",
|
||||
version: "5.7",
|
||||
}}
|
||||
scale={0.6}
|
||||
/>
|
||||
<Avatar
|
||||
char={{
|
||||
name: "Traveler",
|
||||
stars: 5,
|
||||
vision: "anemo",
|
||||
weapon: "sword",
|
||||
image: "1234567890",
|
||||
version: "5.7",
|
||||
}}
|
||||
scale={0.6}
|
||||
/>
|
||||
<Avatar
|
||||
char={{
|
||||
name: "Traveler",
|
||||
stars: 5,
|
||||
vision: "anemo",
|
||||
weapon: "sword",
|
||||
image: "1234567890",
|
||||
version: "5.7",
|
||||
}}
|
||||
scale={0.6}
|
||||
/>
|
||||
<Avatar
|
||||
char={{
|
||||
name: "Traveler",
|
||||
stars: 5,
|
||||
vision: "anemo",
|
||||
weapon: "sword",
|
||||
image: "1234567890",
|
||||
version: "5.7",
|
||||
}}
|
||||
scale={0.6}
|
||||
/>
|
||||
<Avatar
|
||||
char={{
|
||||
name: "Traveler",
|
||||
stars: 5,
|
||||
vision: "anemo",
|
||||
weapon: "sword",
|
||||
image: "1234567890",
|
||||
version: "5.7",
|
||||
}}
|
||||
scale={0.6}
|
||||
/>
|
||||
<Avatar
|
||||
char={{
|
||||
name: "Traveler",
|
||||
stars: 5,
|
||||
vision: "anemo",
|
||||
weapon: "sword",
|
||||
image: "1234567890",
|
||||
version: "5.7",
|
||||
}}
|
||||
scale={0.6}
|
||||
/>
|
||||
<Avatar
|
||||
char={{
|
||||
name: "Traveler",
|
||||
stars: 5,
|
||||
vision: "anemo",
|
||||
weapon: "sword",
|
||||
image: "1234567890",
|
||||
version: "5.7",
|
||||
}}
|
||||
scale={0.6}
|
||||
/>
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { TooltipContent, TooltipTrigger } from "@radix-ui/react-tooltip";
|
||||
import { TvMinimalPlay } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip } from "@/components/ui/tooltip";
|
||||
import { base } from "@/lib/const";
|
||||
import type { YoutubeLiveInfo } from "../api/live/route";
|
||||
|
||||
export async function LiveButton() {
|
||||
const live: YoutubeLiveInfo = await fetch(`${base}/api/live`).then((e) =>
|
||||
e.json(),
|
||||
);
|
||||
if (live === "none") return;
|
||||
const { url, thumbnails } = live;
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link href={url}>
|
||||
<Button variant="outline" type="button">
|
||||
<TvMinimalPlay className="text-red-500 animate-pulse" /> LIVE
|
||||
</Button>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Image
|
||||
height={thumbnails.height / 10}
|
||||
width={thumbnails.width / 10}
|
||||
src={thumbnails.url}
|
||||
alt="yt thumbnail"
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { TooltipTrigger } from "@radix-ui/react-tooltip";
|
||||
import { BookAlert, CircleDollarSign, SendHorizonal } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { Suspense } from "react";
|
||||
import banner from "#/logos/artifact.webp";
|
||||
import { Blocker } from "@/components/blocker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tooltip, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { CharacterChooser } from "./chooser";
|
||||
import { LiveButton } from "./live";
|
||||
|
||||
export default async function ArtifactFormPage() {
|
||||
return (
|
||||
<div className="flex flex-col justify-around items-center h-full">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="justify-center">
|
||||
<CardTitle>
|
||||
<div className="w-[276.5px]">
|
||||
<Image
|
||||
style={{ position: "absolute", transform: "translateY(-70%)" }}
|
||||
height={137.5}
|
||||
width={276.5}
|
||||
src={banner}
|
||||
alt="เสือกไอดีชาวบ้าน"
|
||||
/>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Blocker>
|
||||
<Button variant="destructive" type="button">
|
||||
<BookAlert /> อ่านกฎ
|
||||
</Button>
|
||||
</Blocker>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">ชื่อ</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Mr. Buzz"
|
||||
autoComplete="name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<CharacterChooser />
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="comment">ข้อความเพิ่มเติม</Label>
|
||||
<Textarea id="comment" required placeholder="(ไม่บังคับ)" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex gap-2 justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" type="button">
|
||||
<CircleDollarSign />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>โดเนทลัดคิว ขั้นต่ำ 10 บาท</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="destructive" type="button">
|
||||
<BookAlert />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>อ่านกฎการลงทะเบียน</TooltipContent>
|
||||
</Tooltip>
|
||||
<Suspense>
|
||||
<LiveButton />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button type="submit">
|
||||
<SendHorizonal />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>ส่งเลยคับพี่</TooltipContent>
|
||||
</Tooltip>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+13
-2
@@ -6,7 +6,7 @@
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-sans: var(--font-anuphan);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
@@ -86,7 +86,7 @@
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.546 0.245 262.881);
|
||||
--primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
@@ -120,3 +120,14 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
*:has(.blocker) {
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
}
|
||||
+26
-8
@@ -1,10 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Anuphan, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Image from "next/image";
|
||||
import Background from "#/bg.jpg";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
const anuphan = Anuphan({
|
||||
variable: "--font-anuphan",
|
||||
subsets: ["latin", "thai"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
@@ -13,8 +15,18 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
metadataBase: new URL("https://buzz.gunshiz.top"),
|
||||
title: "เกนชินไม่ใช่เกมมือถือ",
|
||||
description: "ระบบอีเวนท์ของเกนชินไม่ใช่เกมมือถือ",
|
||||
icons: "https://buzz.gunshiz.top/favicon.webp",
|
||||
openGraph: {
|
||||
title: "เกนชินไม่ใช่เกมมือถือ",
|
||||
description: "ระบบอีเวนท์ของเกนชินไม่ใช่เกมมือถือ",
|
||||
url: "https://buzz.gunshiz.top",
|
||||
siteName: "เกนชินไม่ใช่เกมมือถือ",
|
||||
locale: "th_TH",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -23,10 +35,16 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" className="h-full">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
className={`${anuphan.variable} ${geistMono.variable} font-sans antialiased dark h-full`}
|
||||
>
|
||||
<Image
|
||||
src={Background}
|
||||
alt="Background"
|
||||
fill
|
||||
className="z-[-1] opacity-20"
|
||||
/>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user