docs:create .env.exmaple style(admin):change welcome back text, add tooltip for some button change, incrase navbar size

This commit is contained in:
2026-03-12 23:27:47 +07:00 Unverified
parent 5052b05ea2
commit 2e291d871a
8 changed files with 149 additions and 58 deletions
+30
View File
@@ -0,0 +1,30 @@
# Backend
DATABASE_URL=YOUR_DATABASE_URL
REDIS_URL=YOUR_REDIS_URL
BETTER_AUTH_SECRET=YOUR_BETTER_AUTH_SECRET
SENTRY_AUTH_TOKEN=YOURT_SENTRY_AUTH_TOKEN
# Channel target
YOUTUBE_CHANNEL_ID=YOUR_YOUTUBE_CHANNEL_ID
YOUTUBE_API_KEY=YOUR_YOUTUBE_API_KEY
# Base url
BASE_URL=YOUR_BASE_URL # example: https://buzz.sudloh.com
# Discord bot (Rubgram)
DISCORD_BOT_TOKEN=YOUR_DISCORD_BOT_TOKEN
DISCORD_GUILD_ID=YOUR_DISCORD_GUILD_ID
DISCORD_CLIENT_ID=YOUR_DISCORD_CLIENT_ID
DISCORD_CLIENT_SECRET=YOUR_DISCORD_CLIENT_SECRET
DISCORD_REDIRECT_URI=https://YOUR_BASE_URL/rubgram/callback
# Discord webhook (Rubgram)
DISCORD_WEBHOOK_URL=YOUR_DISCORD_WEBHOOK_URL # For Rubgram calling
WEBHOOK_RUBGRAM_SUBMIT=YOUR_WEBHOOK_RUBGRAM_SUBMIT # For Rubgram submit
# Payment (Rubgram) https://slipok.com/
SLIPOK_API_URL=YOUR_SLIPOK_API_URL
SLIPOK_API_KEY=YOUR_SLIPOK_API_KEY
# Environment
ENVIRONMENT=production
+2 -1
View File
@@ -31,7 +31,8 @@ yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* .env
.env.local
!dev/.env.development !dev/.env.development
# vercel # vercel
+7 -3
View File
@@ -1,8 +1,8 @@
# Buzz Events # Buzz Events
This is a Next.js project running multiple events for a YouTuber, Buzz. It have been branched into 5 different parts: This is a Next.js project running multiple events for a YouTuber, Buzz. It have been branched into 5 different parts:
- Artifact: Monthly watcher-submitted ID review - Artifact: Versionly watcher-submitted ID review.
- Tierlist: Versionly tierlist for best characters for Abyss & Stygian - Rubgram: A paid event that basically lets Buzz play endgame content for you.
- Rubgram: A paid event that basically lets Buzz play Abyss/Imaginarium Theater/Stygian for you. - Tierlist: Versionly tierlist rating characters for Spiral Abyss & Stygian Onslaught.
- `WIP` Admin: Admin panel where you can manage versions, characters, tierlists, external services, etc. - `WIP` Admin: Admin panel where you can manage versions, characters, tierlists, external services, etc.
## Architecture ## Architecture
@@ -17,6 +17,10 @@ This is a Next.js project running multiple events for a YouTuber, Buzz. It have
- Authentication: DIscord OAuth2 - Authentication: DIscord OAuth2
- Client side cross components communication with debug menu - Client side cross components communication with debug menu
## External APIs
- Character&Version data: [Project Amber](https://gi.yatta.moe/en)
- Character Card: [Enka.network](https://enka.network/)
## Developers ## Developers
Lead Developer: `@dmgnr` Lead Developer: `@dmgnr`
+8 -5
View File
@@ -6,6 +6,7 @@ import Avatar from "@/components/avatar";
import SearchBox, { type Filters, type ParsedQuery } from "@/components/search"; import SearchBox, { type Filters, type ParsedQuery } from "@/components/search";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { type characters, element } from "@/lib/db/schema"; import { type characters, element } from "@/lib/db/schema";
import { SimpleTooltip } from "@/components/tooltip";
/** Filters and their value providers */ /** Filters and their value providers */
const filters: Filters = { const filters: Filters = {
@@ -46,11 +47,13 @@ export function CharManager({
className="bg-input sticky top-2" className="bg-input sticky top-2"
onQueryChange={setQuery} onQueryChange={setQuery}
/> />
<Button className="h-10 w-10" asChild> <SimpleTooltip text="เพิ่มตัวละคร" side="right">
<Link href="/admin/char/create"> <Button className="h-10 w-10" asChild>
<UserPlus /> <Link href="/admin/char/create">
</Link> <UserPlus />
</Button> </Link>
</Button>
</SimpleTooltip>
</div> </div>
<div className="flex flex-wrap gap-4 justify-center"> <div className="flex flex-wrap gap-4 justify-center">
{filteredChars.map((char) => ( {filteredChars.map((char) => (
+82 -29
View File
@@ -4,45 +4,98 @@ import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { SidebarMenuButton } from "@/components/ui/sidebar"; import { SidebarMenuButton } from "@/components/ui/sidebar";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export function VersionCreateDialogForm() { export function VersionCreateDialogForm() {
return <form>Todo</form>; return <form>Todo</form>;
} }
export function RandomWelcomeMessage({ list }: { list?: string[] }) { export function RandomWelcomeMessage() {
const messages = list || [ const items = [
"What are we cooking today?", {
"I'm NOT an EMERGENCY FOOD!!", "header": "ความลับแห่งแรงจันทราผัน",
"Why are you here?", "message": "แรงจันทราผัน (Kuuvahki) ใน Nod-Krai มีคุณสมบัติเหมือนแรงแม่เหล็ก ซึ่งเป็นกุญแจสำคัญในการแก้ปริศนาโบราณ"
"The emergency food is ready sir.", },
"Who left the artifact stats like that?", {
"Don't forget to hydrate, boss.", "header": "การล่าอันบ้าคลั่ง: Wild Hunt",
"Another day, another reroll.", "message": "เมื่อเผชิญหน้ากับกองทัพ Wild Hunt การลด HP ให้เหลือ 0 เป็นเพียงจุดเริ่มต้นเท่านั้น คุณต้องลดค่า Max HP ของพวกมันให้เหลือ 0 ในขณะที่พวกมันติดสถานะ Grief-Stricken เพื่อกำจัดพวกมันอย่างถาวร "
"Still reviewing artifacts? Respect.", },
"Let's make these builds shine.", {
"You came back… impressive.", "header": "ประกายจันทร์: Ascendant Gleam",
"Processing luck… 0.3% found.", "message": "การจัดทีมที่มีตัวละครจาก Nod-Krai อย่างน้อย 2 ตัว จะเปิดใช้งาน Ascendant Gleam เพื่อให้ใช้ความสามรถตัวละครได้อย่างเต็มที่"
"Someone's gotta keep this event alive.", },
"New submissions incoming!", {
"Welcome back, brave reviewer.", "header": "มรดกแห่งดวงจันทร์ทั้งสาม",
"Did you bring Mora this time?", "message": "Nod-Krai เคยถูกเรียกว่า Nephilheim และถูกเชื่อว่าเป็นเศษเสี้ยวของดวงจันทร์ดวงที่สี่ซึ่งแตกสลายก่อนจะก่อตัวเสร็จสิ้น ทำให้ดินแดนแห่งนี้มีความเชื่อมโยงกับพลังจันทราอย่างลึกซึ้ง"
"Your approval power is unmatched.", },
"Remember: critique kindly, roast fairly.", {
"Time to bless—or curse—some artifacts.", "header": "คำสาบานของผู้เฝ้าประภาคาร",
"Don't worry, no 5★ drama yet.", "message": "กลุ่ม Lightkeepers ยึดถือคติ 'ใช้เลือดและกระดูกเป็นเชื้อเพลิง' พวกเขาเฝ้าประภาคารที่ Piramida มานานกว่า 500 ปีเพื่อไม่ให้ความมืดจาก Abyss ล่มสลายดินแดนของมนุษย์"
"Admin mode: activated.", },
]; {
"header": "อาวุธชำระล้าง Terpikeraunas",
"message": "Arrow of Terpikeraunas เป็นโบราณวัตถุจาก Hyperborea ที่สามารถเปลี่ยนพลังงานจาก Light Realm ให้เป็นเปลวไฟที่ใช้บริสุทธิ์พลังงานจาก Void Realm (Abyss) ได้"
},
{
"header": "วงล้อจันทรา: สิ่งมาก่อนวิชั่น",
"message": "ใน Nod-Krai ผู้คนอาจได้รับ 'Moon Wheel' (กงล้อจันทรา) ซึ่งเป็นอุปกรณ์ที่ช่วยให้สามารถควบคุมแรงจันทราผันเพื่อปกป้องตนเองในถิ่นทุรกันดาร"
},
{
"header": "การรุกรานของ Rerir",
"message": "Rerir หนึ่งในห้ามหาบาปแห่ง Khaenri'ah เป็นผู้อยู่เบื้องหลังการโจมตีของ Wild Hunt เขามีเป้าหมายในการรวบรวมชิ้นส่วนของดวงจันทร์เพื่อฟื้นฟูร่างกายที่แตกสลายของตนเอง "
},
{
"header": "เมืองสีทองที่ล่มสลาย Hyperborea",
"message": "Hyperborea ตั้งอยู่ทางตอนเหนือสุดของ Nod-Krai เมืองนี้ล่มสลายลงหลังโดน Celestial Nail ปักใส่ ปัจจุบันกลายเป็นซากปรักหักพังที่เต็มไปด้วยความลับของภาพสะท้อนแห่งสวรรค์ "
},
{
"header": "นวัตกรรมจาก Clink-Clank Krumkake",
"message": "ร้านงานฝีมือ Krumkake เป็นศูนย์กลางของการประดิษฐ์ใน Nod-Krai มีการผสมผสานเทคโนโลยีเฟืองจาก Fontaine และพลังงานคูวากิเพื่อสร้างอุปกรณ์สำรวจที่ไม่มีใครเหมือน "
},
{
"header": "ความเชื่อของลูกหลานจันทราน้ำค้างแข็ง",
"message": "ลูกหลานจันทราน้ำค้างแข็งได้เคารพศรัทธาต่อ 'เทพจันทรา' และได้ละทิ้งอารยธรรมทองคำยุคเก่า"
},
// {
// "header": "สายใยครอบครัวในดินแดน Elysium",
// "message": "Nod-Krai ได้รับฉายาว่า Elysium เนื่องจากเป็นที่รวมตัวของคนนอกกฎหมายและนักผจญภัยจากทั่ว Teyvat ธีมหลักของตัวละครที่นี่จึงมักเป็นเรื่องของ 'ครอบครัวที่เลือกเอง' (Found Family) "
// },
{
"header": "นกไนติงเกลของ Solovei",
"message": "ตราสัญลักษณ์ของผู้เฝ้าประภาคารคือ นกไนติงเกลคาบไส้ตะเกียงที่ลุกไหม้ ซึ่งมาจากสัญลักษณ์ประจำตระกูลของ Solovei ผู้ก่อตั้งองค์กรและ Starshyna คนแรก "
},
{
"header": "จันทราทั้ง 3",
"message": "ในอดีตของ Teyvat เคยมีดวงจันทร์ทั้ง 3 ได้แก่ จันทราน้ำค้างแข็ง จันทราสีรุ้ง และจันทรานิรันดร์"
},
{
"header": "Genshin impact",
"message": "Action RPG แบบ Open World สไตล์อนิเมะ ที่โดดเด่นด้วยระบบการต่อสู้โดยใช้พลังธาตุและการสำรวจโลกกว้างที่อิสระ"
}
];
const [message, setMessage] = useState(""); const [index, setIndex] = useState(-1);
// biome-ignore lint/correctness/useExhaustiveDependencies: false useEffect(() => {
useEffect( setIndex(Math.floor(Math.random() * items.length));
() => setMessage(messages[Math.floor(Math.random() * messages.length)]), }, [items.length]);
[messages.length],
if (index === -1) return (
<div className="flex flex-col gap-0.5 mt-1 items-center">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-4 w-64" />
</div>
); );
return message; const item = items[index];
return (
<div className="flex flex-col gap-0.5 mt-1">
<div className="text-sm flex items-center justify-center">{item.header}</div>
<div className="opacity-60 text-xs ">{item.message}</div>
</div>
);
} }
export function SidebarLink({ export function SidebarLink({
+15 -15
View File
@@ -105,17 +105,17 @@ function DropdownMenu({
type="button" type="button"
onClick={() => setOpenSub(isOpen ? null : item.name)} onClick={() => setOpenSub(isOpen ? null : item.name)}
className={cn( className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-left", "flex items-center gap-2 w-full px-2.5 py-2 rounded-md text-left",
"transition-colors text-xs", "transition-colors text-[13px]",
active active
? "bg-white/15 text-white" ? "bg-white/15 text-white"
: "text-white/60 hover:text-white hover:bg-white/10", : "text-white/60 hover:text-white hover:bg-white/10",
)} )}
> >
<Icon size={14} name={item.name} className="shrink-0" /> <Icon size={16} name={item.name} className="shrink-0" />
<span className="flex-1">{item.name}</span> <span className="flex-1">{item.name}</span>
<ChevronRight <ChevronRight
size={12} size={14}
className={cn( className={cn(
"transition-transform", "transition-transform",
isOpen && "rotate-90", isOpen && "rotate-90",
@@ -136,14 +136,14 @@ function DropdownMenu({
href={item.href!} href={item.href!}
onClick={onClose} onClick={onClose}
className={cn( className={cn(
"flex items-center gap-2 px-2 py-1.5 rounded-md", "flex items-center gap-2 px-2.5 py-2 rounded-md",
"transition-colors text-xs", "transition-colors text-[13px]",
active active
? "bg-white/15 text-white" ? "bg-white/15 text-white"
: "text-white/60 hover:text-white hover:bg-white/10", : "text-white/60 hover:text-white hover:bg-white/10",
)} )}
> >
<Icon size={14} name={item.name} className="shrink-0" /> <Icon size={16} name={item.name} className="shrink-0" />
<span>{item.name}</span> <span>{item.name}</span>
</Link> </Link>
)} )}
@@ -262,7 +262,7 @@ export function AdminNavbar({
setScopeOverride(scopeOverride === "root" ? null : "root") setScopeOverride(scopeOverride === "root" ? null : "root")
} }
className={cn( className={cn(
"flex items-center justify-center w-6 h-6 rounded transition-colors opacity-50 hover:opacity-100", "flex items-center justify-center w-6 h-8 rounded transition-colors opacity-50 hover:opacity-100",
scopeOverride === "root" && "opacity-100 bg-white/15", scopeOverride === "root" && "opacity-100 bg-white/15",
)} )}
> >
@@ -273,18 +273,18 @@ export function AdminNavbar({
type="button" type="button"
onClick={() => setShowLabels((v) => !v)} onClick={() => setShowLabels((v) => !v)}
className={cn( className={cn(
"flex items-center justify-center w-6 h-6 rounded transition-colors", "flex items-center justify-center w-8 h-8 rounded transition-colors",
showLabels showLabels
? "bg-white/15 text-white" ? "bg-white/15 text-white"
: "text-white/40 hover:text-white/70", : "text-white/40 hover:text-white/70",
)} )}
> >
{showLabels ? <X size={13} /> : <AlignJustify size={13} />} {showLabels ? <X size={16} /> : <AlignJustify size={16} />}
</button> </button>
<div className="w-px h-4 bg-white/10 mx-0.5" /> <div className="w-px h-5 bg-white/10 mx-1" />
</div> </div>
<div className="flex items-center gap-0.5 overflow-x-auto scrollbar-none flex-1 min-w-0"> <div className="flex items-center gap-1 overflow-x-auto scrollbar-none flex-1 min-w-0">
{(activeParent ? activeParent.sub! : items).map((item) => { {(activeParent ? activeParent.sub! : items).map((item) => {
const Icon = item.icon; const Icon = item.icon;
const active = isActive(item, pathname); const active = isActive(item, pathname);
@@ -293,9 +293,9 @@ export function AdminNavbar({
const buttonContent = ( const buttonContent = (
<> <>
<Icon size={15} name={item.name} className="shrink-0" /> <Icon size={18} name={item.name} className="shrink-0" />
{showLabels && ( {showLabels && (
<span className="text-[10px] whitespace-nowrap leading-none"> <span className="text-[11px] whitespace-nowrap leading-none">
{item.name} {item.name}
</span> </span>
)} )}
@@ -303,7 +303,7 @@ export function AdminNavbar({
); );
const buttonClass = cn( const buttonClass = cn(
"flex items-center gap-1 px-1.5 py-1 rounded transition-colors shrink-0", "flex items-center gap-1.5 px-2.5 py-1.5 rounded transition-colors shrink-0",
active || isOpen active || isOpen
? "bg-white/15 text-white" ? "bg-white/15 text-white"
: "text-white/50 hover:text-white hover:bg-white/10", : "text-white/50 hover:text-white hover:bg-white/10",
+4 -4
View File
@@ -3,11 +3,11 @@ import { RandomWelcomeMessage } from "./client";
export default function AdminPage() { export default function AdminPage() {
return ( return (
<div className="flex h-full w-full justify-center items-center"> <div className="flex h-full w-full justify-center items-center">
<div className="flex flex-col"> <div className="flex flex-col items-center">
<span className="opacity-80 text-4xl font-bold">ยินดีต้อนรับกลับ!</span> <span className="text-4xl font-bold no-translate">ยินดีต้อนรับกลับ!</span>
<span> <div className="mt-1">
<RandomWelcomeMessage /> <RandomWelcomeMessage />
</span> </div>
</div> </div>
</div> </div>
); );
+1 -1
View File
@@ -1,5 +1,5 @@
import { Section } from "./page"; import { Section } from "./page";
export function SettingsAccountSection() { export function SettingsAccountSection() {
return <Section title="บัญชี">ask dreamgineer bruh</Section>; return <Section title="บัญชี">ยังไม่พร้อมให้บริการ</Section>;
} }