feat: add YouTube social link to user profiles (#114)
* feat: add YouTube social link to user profiles - Add youtube_url column to auth_users schema - Add DB migration for the new column - Update updateProfile action with YouTube URL validation - Update getUserProfile and getPublicProfile to include youtubeUrl - Add YouTube input field to settings page - Display YouTube icon on public profile pages * fix: use drizzle-kit generate for youtube_url migration Replaces manually created migration with proper drizzle-kit generated migration that includes meta snapshot and journal entry. --------- Co-authored-by: Anton Pascal <anton-pascal@users.noreply.github.com>
This commit is contained in:
@@ -32,6 +32,7 @@ export default async function Settings() {
|
||||
currentUsername={profile?.username ?? null}
|
||||
currentGithubUrl={profile?.githubUrl ?? null}
|
||||
currentXUrl={profile?.xUrl ?? null}
|
||||
currentYoutubeUrl={profile?.youtubeUrl ?? null}
|
||||
currentEmailNotifications={profile?.emailNotifications ?? true}
|
||||
connectedAccounts={connectedAccounts}
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,14 @@ function XIcon({ className }: { className?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function YouTubeIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
interface PublicProfilePageProps {
|
||||
profile: {
|
||||
id: string
|
||||
@@ -31,6 +39,7 @@ interface PublicProfilePageProps {
|
||||
username: string
|
||||
githubUrl: string | null
|
||||
xUrl: string | null
|
||||
youtubeUrl: string | null
|
||||
}
|
||||
projects: Project[]
|
||||
}
|
||||
@@ -88,7 +97,7 @@ export function PublicProfilePage({ profile, projects }: PublicProfilePageProps)
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold">{profile.name}</h1>
|
||||
<p className="text-muted-foreground">@{profile.username}</p>
|
||||
{(profile.githubUrl || profile.xUrl) && (
|
||||
{(profile.githubUrl || profile.xUrl || profile.youtubeUrl) && (
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
{profile.githubUrl && (
|
||||
<a
|
||||
@@ -110,6 +119,16 @@ export function PublicProfilePage({ profile, projects }: PublicProfilePageProps)
|
||||
<XIcon className="h-5 w-5" />
|
||||
</a>
|
||||
)}
|
||||
{profile.youtubeUrl && (
|
||||
<a
|
||||
href={profile.youtubeUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<YouTubeIcon className="h-5 w-5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@ interface SettingsPageProps {
|
||||
currentUsername: string | null
|
||||
currentGithubUrl: string | null
|
||||
currentXUrl: string | null
|
||||
currentYoutubeUrl: string | null
|
||||
currentEmailNotifications: boolean
|
||||
connectedAccounts: { providerId: string; accountId: string }[]
|
||||
}
|
||||
@@ -49,12 +50,14 @@ export function SettingsPage({
|
||||
currentUsername,
|
||||
currentGithubUrl,
|
||||
currentXUrl,
|
||||
currentYoutubeUrl,
|
||||
currentEmailNotifications,
|
||||
connectedAccounts,
|
||||
}: SettingsPageProps) {
|
||||
const [username, setUsername] = useState(currentUsername ?? '')
|
||||
const [githubUrl, setGithubUrl] = useState(currentGithubUrl ?? '')
|
||||
const [xUrl, setXUrl] = useState(currentXUrl ?? '')
|
||||
const [youtubeUrl, setYoutubeUrl] = useState(currentYoutubeUrl ?? '')
|
||||
const [avatarUrl, setAvatarUrl] = useState(user.image)
|
||||
const [isSavingUsername, setIsSavingUsername] = useState(false)
|
||||
const [isSavingSocial, setIsSavingSocial] = useState(false)
|
||||
@@ -138,6 +141,7 @@ export function SettingsPage({
|
||||
const result = await updateProfile({
|
||||
githubUrl: githubUrl.trim() || null,
|
||||
xUrl: xUrl.trim() || null,
|
||||
youtubeUrl: youtubeUrl.trim() || null,
|
||||
})
|
||||
setSocialMessage({
|
||||
type: result.success ? 'success' : 'error',
|
||||
@@ -151,7 +155,8 @@ export function SettingsPage({
|
||||
const usernameChanged = username.trim() !== (currentUsername ?? '')
|
||||
const socialChanged =
|
||||
(githubUrl.trim() || '') !== (currentGithubUrl ?? '') ||
|
||||
(xUrl.trim() || '') !== (currentXUrl ?? '')
|
||||
(xUrl.trim() || '') !== (currentXUrl ?? '') ||
|
||||
(youtubeUrl.trim() || '') !== (currentYoutubeUrl ?? '')
|
||||
|
||||
const handleToggleEmailNotifications = async () => {
|
||||
const newValue = !emailNotifications
|
||||
@@ -389,6 +394,24 @@ export function SettingsPage({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="youtube" className="font-medium text-sm">
|
||||
YouTube
|
||||
</label>
|
||||
<input
|
||||
id="youtube"
|
||||
type="url"
|
||||
value={youtubeUrl}
|
||||
onChange={(e) => {
|
||||
setYoutubeUrl(e.target.value)
|
||||
setSocialMessage(null)
|
||||
}}
|
||||
placeholder="https://youtube.com/@yourchannel"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={isSavingSocial}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{socialMessage && (
|
||||
<div
|
||||
className={`rounded-md border p-3 text-sm ${
|
||||
|
||||
@@ -122,6 +122,7 @@ export async function getUserProfile(): Promise<{
|
||||
username: string | null
|
||||
githubUrl: string | null
|
||||
xUrl: string | null
|
||||
youtubeUrl: string | null
|
||||
emailNotifications: boolean
|
||||
} | null> {
|
||||
const session = await getSession()
|
||||
@@ -132,6 +133,7 @@ export async function getUserProfile(): Promise<{
|
||||
username: schema.users.username,
|
||||
githubUrl: schema.users.githubUrl,
|
||||
xUrl: schema.users.xUrl,
|
||||
youtubeUrl: schema.users.youtubeUrl,
|
||||
emailNotifications: schema.users.emailNotifications,
|
||||
})
|
||||
.from(schema.users)
|
||||
@@ -147,6 +149,7 @@ export async function getUserProfile(): Promise<{
|
||||
export async function updateProfile(data: {
|
||||
githubUrl?: string | null
|
||||
xUrl?: string | null
|
||||
youtubeUrl?: string | null
|
||||
}): Promise<{ success: boolean; error?: string }> {
|
||||
const session = await getSession()
|
||||
if (!session?.user) {
|
||||
@@ -159,12 +162,16 @@ export async function updateProfile(data: {
|
||||
if (data.xUrl && !/^https:\/\/(www\.)?(x|twitter)\.com\/.+/.test(data.xUrl)) {
|
||||
return { success: false, error: 'Invalid X/Twitter URL' }
|
||||
}
|
||||
if (data.youtubeUrl && !/^https:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+/.test(data.youtubeUrl)) {
|
||||
return { success: false, error: 'Invalid YouTube URL' }
|
||||
}
|
||||
|
||||
await db
|
||||
.update(schema.users)
|
||||
.set({
|
||||
githubUrl: data.githubUrl ?? null,
|
||||
xUrl: data.xUrl ?? null,
|
||||
youtubeUrl: data.youtubeUrl ?? null,
|
||||
})
|
||||
.where(eq(schema.users.id, session.user.id))
|
||||
|
||||
@@ -184,6 +191,7 @@ export async function getPublicProfile(username: string): Promise<{
|
||||
username: string
|
||||
githubUrl: string | null
|
||||
xUrl: string | null
|
||||
youtubeUrl: string | null
|
||||
}
|
||||
error?: string
|
||||
}> {
|
||||
@@ -195,6 +203,7 @@ export async function getPublicProfile(username: string): Promise<{
|
||||
username: schema.users.username,
|
||||
githubUrl: schema.users.githubUrl,
|
||||
xUrl: schema.users.xUrl,
|
||||
youtubeUrl: schema.users.youtubeUrl,
|
||||
})
|
||||
.from(schema.users)
|
||||
.where(sql`lower(${schema.users.username}) = lower(${username})`)
|
||||
|
||||
@@ -19,6 +19,8 @@ export const users = pgTable(
|
||||
githubUrl: t.text('github_url'),
|
||||
/** X/Twitter profile URL */
|
||||
xUrl: t.text('x_url'),
|
||||
/** YouTube channel URL */
|
||||
youtubeUrl: t.text('youtube_url'),
|
||||
/** Whether the user wants to receive email notifications about new features and updates */
|
||||
emailNotifications: t.boolean('email_notifications').notNull().default(true),
|
||||
role: userRoles('role').notNull().default('user'),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "auth_users" ADD COLUMN "youtube_url" text;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,13 @@
|
||||
"when": 1771536409437,
|
||||
"tag": "20260219212649_young_dragon_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1771911353115,
|
||||
"tag": "20260224053553_stormy_carnage",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user