feat: complete new UI for Profiles, fix notfound error on username change

This commit is contained in:
Hamzat Victor
2025-01-06 02:52:32 +01:00
parent 024e30bd99
commit ceeb1fbce3
12 changed files with 784 additions and 263 deletions

View File

@ -2,6 +2,9 @@
import { revalidatePath } from "next/cache"
import { z } from "zod"
import { editUserSchema } from "./schema"
import { UserLink } from "./types"
import { parseSocialLink } from "./utils"
export async function createSandbox(body: {
type: string
@ -123,20 +126,31 @@ const UpdateErrorSchema = z.object({
.optional(),
})
export async function updateUser(prevState: any, formData: FormData) {
const data = Object.fromEntries(formData)
const schema = z.object({
id: z.string(),
username: z.string(),
oldUsername: z.string(),
name: z.string(),
interface FormState {
message: string
error?: any
newRoute?: string
fields?: Record<string, unknown>
}
export async function updateUser(
prevState: any,
formData: FormData
): Promise<FormState> {
let data = Object.fromEntries(formData)
let links: UserLink[] = []
Object.entries(data).forEach(([key, value]) => {
if (key.startsWith("link")) {
const [_, index] = key.split(".")
if (value) {
links.splice(parseInt(index), 0, parseSocialLink(value as string))
delete data[key]
}
}
})
console.log(data)
// @ts-ignore
data.links = links
try {
const validatedData = schema.parse(data)
const validatedData = editUserSchema.parse(data)
const changedUsername = validatedData.username !== validatedData.oldUsername
const res = await fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`,
@ -150,6 +164,9 @@ export async function updateUser(prevState: any, formData: FormData) {
id: validatedData.id,
username: data.username ?? undefined,
name: data.name ?? undefined,
bio: data.bio ?? undefined,
personalWebsite: data.personalWebsite ?? undefined,
links: data.links ?? undefined,
}),
}
)
@ -160,11 +177,11 @@ export async function updateUser(prevState: any, formData: FormData) {
const parseResult = UpdateErrorSchema.safeParse(responseData)
if (!parseResult.success) {
return { error: "Unexpected error occurred" }
}
if (parseResult.data.error) {
return parseResult.data
return {
message: "Unexpected error occurred",
error: parseResult.error,
fields: validatedData,
}
}
if (changedUsername) {
@ -175,12 +192,13 @@ export async function updateUser(prevState: any, formData: FormData) {
return { message: "Successfully updated" }
} catch (error) {
if (error instanceof z.ZodError) {
console.log(error)
return {
error: error.errors?.[0].message,
message: "Invalid data",
error: error.errors,
fields: data,
}
}
return { error: "An unexpected error occurred" }
return { message: "An unexpected error occurred", fields: data }
}
}

View File

@ -0,0 +1,14 @@
export const KNOWN_PLATFORMS = [
"github",
"twitter",
"instagram",
"bluesky",
"linkedin",
"youtube",
"twitch",
"discord",
"mastodon",
"threads",
"gitlab",
"generic",
] as const

View File

@ -1,3 +1,37 @@
import {
AtSign,
Github,
GitlabIcon as GitlabLogo,
Globe,
Instagram,
Link,
Linkedin,
MessageCircle,
Twitch,
Twitter,
Youtube,
} from "lucide-react"
import { KnownPlatform } from "../types"
export const socialIcons: Record<
KnownPlatform | "website",
React.ComponentType<any>
> = {
github: Github,
twitter: Twitter,
instagram: Instagram,
bluesky: AtSign,
linkedin: Linkedin,
youtube: Youtube,
twitch: Twitch,
discord: MessageCircle,
mastodon: AtSign,
threads: AtSign,
gitlab: GitlabLogo,
generic: Link,
website: Globe,
}
export const projectTemplates: {
id: string
name: string

View File

@ -0,0 +1,20 @@
import { z } from "zod"
import { KNOWN_PLATFORMS } from "../constants"
export const editUserSchema = z.object({
id: z.string().trim(),
username: z.string().trim().min(1, "Username must be at least 1 character"),
oldUsername: z.string().trim(),
name: z.string().trim().min(1, "Name must be at least 1 character"),
bio: z.string().trim().optional(),
personalWebsite: z.string().trim().optional(),
links: z
.array(
z.object({
url: z.string().trim(),
platform: z.enum(KNOWN_PLATFORMS),
})
)
.catch([]),
})
export type EditUserSchema = z.infer<typeof editUserSchema>

View File

@ -1,5 +1,7 @@
// DB Types
import { KNOWN_PLATFORMS } from "./constants"
export type User = {
id: string
name: string
@ -8,11 +10,20 @@ export type User = {
avatarUrl: string | null
createdAt: Date
generations: number
sandbox: Sandbox[]
usersToSandboxes: UsersToSandboxes[]
bio: string | null
personalWebsite: string | null
links: UserLink[]
tier: "FREE" | "PRO" | "ENTERPRISE"
tierExpiresAt: Date
lastResetDate?: number
lastResetDate: number
sandbox: Sandbox[]
usersToSandboxes: UsersToSandboxes[]
}
export type KnownPlatform = (typeof KNOWN_PLATFORMS)[number]
export type UserLink = {
url: string
platform: KnownPlatform
}
export type Sandbox = {

View File

@ -2,7 +2,7 @@ import { type ClassValue, clsx } from "clsx"
// import { toast } from "sonner"
import { twMerge } from "tailwind-merge"
import fileExtToLang from "./file-extension-to-language.json"
import { TFile, TFolder } from "./types"
import { KnownPlatform, TFile, TFolder, UserLink } from "./types"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@ -98,3 +98,57 @@ export function sortFileExplorer(
return item
})
}
export function parseSocialLink(url: string): UserLink {
try {
// Handle empty or invalid URLs
if (!url) return { url: "", platform: "generic" }
// Remove protocol and www prefix for consistent parsing
const cleanUrl = url
.toLowerCase()
.replace(/^https?:\/\//, "")
.replace(/^www\./, "")
.split("/")[0] // Get just the domain part
// Platform detection mapping
const platformPatterns: Record<
Exclude<KnownPlatform, "generic">,
RegExp
> = {
github: /github\.com/,
twitter: /(?:twitter\.com|x\.com|t\.co)/,
instagram: /instagram\.com/,
bluesky: /(?:bsky\.app|bluesky\.social)/,
linkedin: /linkedin\.com/,
youtube: /(?:youtube\.com|youtu\.be)/,
twitch: /twitch\.tv/,
discord: /discord\.(?:gg|com)/,
mastodon: /mastodon\.(?:social|online|world)/,
threads: /threads\.net/,
gitlab: /gitlab\.com/,
}
// Check URL against each pattern
for (const [platform, pattern] of Object.entries(platformPatterns)) {
if (pattern.test(cleanUrl)) {
return {
url,
platform: platform as KnownPlatform,
}
}
}
// Fall back to generic if no match found
return {
url,
platform: "generic",
}
} catch (error) {
console.error("Error parsing social link:", error)
return {
url: url || "",
platform: "generic",
}
}
}