add social links support

This commit is contained in:
2025-06-21 15:00:40 +02:00
parent 228cf6caa3
commit 86014e261b
6 changed files with 185 additions and 24 deletions

View File

@@ -1,11 +1,5 @@
'use client' 'use client'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -32,6 +26,7 @@ import {
} from '@/components/ui/select' } from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { auth } from '@/server/auth'
import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants' import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants'
import { api } from '@/trpc/react' import { api } from '@/trpc/react'
import { import {
@@ -40,15 +35,16 @@ import {
BarChart3, BarChart3,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
EllipsisVertical,
Filter, Filter,
IceCreamCone, IceCreamCone,
ShieldHalf, ShieldHalf,
Star, Star,
Trophy, Trophy,
Twitch,
UserIcon, UserIcon,
Youtube,
} from 'lucide-react' } from 'lucide-react'
import { ExternalIcon } from 'next/dist/client/components/react-dev-overlay/ui/icons/external' import { useSession } from 'next-auth/react'
import Link from 'next/link' import Link from 'next/link'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
import { isNonNullish } from 'remeda' import { isNonNullish } from 'remeda'
@@ -206,20 +202,6 @@ export function UserInfo() {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
{/*<DropdownMenu>*/}
{/* <DropdownMenuTrigger asChild>*/}
{/* <Button variant={'ghost'} size={'iconSm'}>*/}
{/* <EllipsisVertical className={'size-4'} />*/}
{/* </Button>*/}
{/* </DropdownMenuTrigger>*/}
{/* <DropdownMenuContent>*/}
{/* <DropdownMenuItem asChild>*/}
{/* <Link href={`/stream-card/${id}`} target={'_blank'}>*/}
{/* Stream widget <ExternalIcon />*/}
{/* </Link>*/}
{/* </DropdownMenuItem>*/}
{/* </DropdownMenuContent>*/}
{/*</DropdownMenu>*/}
</div> </div>
<p className='pt-2 text-gray-500 text-sm dark:text-zinc-400'> <p className='pt-2 text-gray-500 text-sm dark:text-zinc-400'>
@@ -258,6 +240,38 @@ export function UserInfo() {
</span> </span>
</Badge> </Badge>
)} )}
{discord_user.twitch_url && (
<Badge
variant='outline'
className='border-gray-200 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800'
>
<Twitch className='mr-1 h-3 w-3 text-purple-500' />
<a
href={discord_user.twitch_url}
target='_blank'
rel='noopener noreferrer'
className='text-gray-700 hover:underline dark:text-zinc-300'
>
Twitch
</a>
</Badge>
)}
{discord_user.youtube_url && (
<Badge
variant='outline'
className='border-gray-200 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800'
>
<Youtube className='mr-1 h-3 w-3 text-red-500' />
<a
href={discord_user.youtube_url}
target='_blank'
rel='noopener noreferrer'
className='text-gray-700 hover:underline dark:text-zinc-300'
>
YouTube
</a>
</Badge>
)}
</div> </div>
</div> </div>
<div <div

View File

@@ -0,0 +1,116 @@
'use client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { api } from '@/trpc/react'
import { SiTwitch, SiYoutube } from '@icons-pack/react-simple-icons'
import { ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'sonner'
export function ProfileSettingsPageClient({
userId,
}: { userId: string | null }) {
const [socialLinks] = api.profile.getSocialLinks.useSuspenseQuery()
const [twitchUrl, setTwitchUrl] = useState<string | null>(
socialLinks.twitch_url
)
const [youtubeUrl, setYoutubeUrl] = useState<string | null>(
socialLinks.youtube_url
)
const { mutate: updateSocialLinks, isPending } =
api.profile.updateSocialLinks.useMutation({
onSuccess: () => {
toast.success('Social links updated successfully')
},
})
const handleSave = () => {
updateSocialLinks({
twitch_url: twitchUrl,
youtube_url: youtubeUrl,
})
}
return (
<div className='mx-auto flex w-[calc(100%-1rem)] max-w-fd-container flex-1 flex-col py-8'>
<div className='mb-6'>
<h1 className='font-bold text-3xl text-gray-900 dark:text-white'>
Profile Settings
</h1>
<p className='mt-2 text-gray-500 dark:text-zinc-400'>
Manage your profile settings and social links
</p>
</div>
<div className='space-y-6'>
<div className='rounded-lg border bg-white p-6 dark:bg-zinc-800/20'>
<h2 className='mb-4 font-semibold text-xl'>Stream Widget</h2>
<p className='mb-4 text-gray-500 dark:text-zinc-400'>
Use this widget to display your stats on your stream
</p>
{userId && (
<Link
href={`/stream-card/${userId}`}
target='_blank'
className='inline-flex items-center gap-2 text-violet-600 hover:text-violet-700 dark:text-violet-400 dark:hover:text-violet-300'
>
Open Stream Widget <ExternalLink className='h-4 w-4' />
</Link>
)}
</div>
<form
onSubmit={(e) => {
e.preventDefault()
handleSave()
}}
className='rounded-lg border bg-white p-6 dark:bg-zinc-800/20'
>
<h2 className='mb-4 font-semibold text-xl'>Social Links</h2>
<div className='space-y-4'>
<div>
<Label className='mb-2' htmlFor={'ttv-url'}>
Twitch URL
</Label>
<div className='flex items-center gap-2'>
<SiTwitch className='h-5 w-5 text-purple-500' />
<Input
id={'ttv-url'}
type='url'
value={twitchUrl || ''}
onChange={(e) => setTwitchUrl(e.target.value || null)}
placeholder='https://twitch.tv/yourusername'
/>
</div>
</div>
<div>
<Label className='mb-2' htmlFor={'yt-url'}>
YouTube URL
</Label>
<div className='flex items-center gap-2'>
<SiYoutube className='h-5 w-5 text-red-500' />
<Input
id={'yt-url'}
type='url'
value={youtubeUrl || ''}
onChange={(e) => setYoutubeUrl(e.target.value || null)}
placeholder='https://youtube.com/c/yourchannel'
className='w-full rounded-md border border-gray-300 p-2 dark:border-zinc-700 dark:bg-zinc-900'
/>
</div>
</div>
</div>
<div className='mt-6 flex justify-end gap-2'>
<Button disabled={isPending} type={'submit'}>
{isPending ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</form>
</div>
</div>
)
}

View File

@@ -13,7 +13,7 @@ import { ThemeToggle } from 'fumadocs-ui/components/layout/theme-toggle'
import type { HomeLayoutProps } from 'fumadocs-ui/layouts/home' import type { HomeLayoutProps } from 'fumadocs-ui/layouts/home'
import type { LinkItemType } from 'fumadocs-ui/layouts/links' import type { LinkItemType } from 'fumadocs-ui/layouts/links'
import { replaceOrDefault } from 'fumadocs-ui/layouts/shared' import { replaceOrDefault } from 'fumadocs-ui/layouts/shared'
import { LogIn, LogOut, Tv, User } from 'lucide-react' import { LogIn, LogOut, Settings, Tv, User } from 'lucide-react'
import { signIn, signOut, useSession } from 'next-auth/react' import { signIn, signOut, useSession } from 'next-auth/react'
import Link from 'next/link' import Link from 'next/link'
import { Fragment } from 'react' import { Fragment } from 'react'
@@ -106,6 +106,15 @@ export function Header({
<span>Profile</span> <span>Profile</span>
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
href='/profile/settings'
className='flex w-full items-center'
>
<Settings className='mr-2 h-4 w-4' />
<span>Settings</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link <Link
href={`/stream-card/${session.user.discord_id}`} href={`/stream-card/${session.user.discord_id}`}

View File

@@ -3,6 +3,7 @@ import { discord_router } from '@/server/api/routers/discord'
import { history_router } from '@/server/api/routers/history' import { history_router } from '@/server/api/routers/history'
import { leaderboard_router } from '@/server/api/routers/leaderboard' import { leaderboard_router } from '@/server/api/routers/leaderboard'
import { playerStateRouter } from '@/server/api/routers/player-state' import { playerStateRouter } from '@/server/api/routers/player-state'
import { profileRouter } from '@/server/api/routers/profile'
import { releasesRouter } from '@/server/api/routers/releases' import { releasesRouter } from '@/server/api/routers/releases'
import { createCallerFactory, createTRPCRouter } from '@/server/api/trpc' import { createCallerFactory, createTRPCRouter } from '@/server/api/trpc'
@@ -17,6 +18,7 @@ export const appRouter = createTRPCRouter({
discord: discord_router, discord: discord_router,
leaderboard: leaderboard_router, leaderboard: leaderboard_router,
playerState: playerStateRouter, playerState: playerStateRouter,
profile: profileRouter,
releases: releasesRouter, releases: releasesRouter,
}) })

View File

@@ -1,5 +1,8 @@
import { createTRPCRouter, publicProcedure } from '@/server/api/trpc' import { createTRPCRouter, publicProcedure } from '@/server/api/trpc'
import { db } from '@/server/db'
import { users } from '@/server/db/schema'
import { discord_service } from '@/server/services/discord.service' import { discord_service } from '@/server/services/discord.service'
import { eq } from 'drizzle-orm'
import { z } from 'zod' import { z } from 'zod'
export const discord_router = createTRPCRouter({ export const discord_router = createTRPCRouter({
@@ -10,6 +13,21 @@ export const discord_router = createTRPCRouter({
}) })
) )
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
return await discord_service.get_user_by_id(input.user_id) const discordUser = await discord_service.get_user_by_id(input.user_id)
// Get social media links from the database
const userData = await db.query.users.findFirst({
where: eq(users.discord_id, input.user_id),
columns: {
twitch_url: true,
youtube_url: true,
},
})
return {
...discordUser,
twitch_url: userData?.twitch_url || null,
youtube_url: userData?.youtube_url || null,
}
}), }),
}) })

View File

@@ -63,6 +63,8 @@ export const users = pgTable('user', (d) => ({
.default(sql`CURRENT_TIMESTAMP`), .default(sql`CURRENT_TIMESTAMP`),
image: d.varchar({ length: 255 }), image: d.varchar({ length: 255 }),
discord_id: d.varchar({ length: 255 }), discord_id: d.varchar({ length: 255 }),
twitch_url: d.varchar({ length: 255 }),
youtube_url: d.varchar({ length: 255 }),
role: d.varchar({ length: 255 }).notNull().default('user'), role: d.varchar({ length: 255 }).notNull().default('user'),
})) }))