From 604e1ac03b8e10caef29beaa1d2ba41f323be6dc Mon Sep 17 00:00:00 2001 From: Optirx Date: Thu, 15 Jan 2026 12:49:44 +0200 Subject: [PATCH] landing --- .claude/settings.local.json | 3 +- src/App.jsx | 254 ++++++++++++------- src/components/AdminLayout.jsx | 39 +-- src/pages/Home.jsx | 15 +- src/pages/Landing.jsx | 405 +++++++++++++++++++++++++++++++ src/pages/Login.jsx | 176 ++++++++++++++ src/pages/Signup.jsx | 429 +++++++++++++++++++++++++++++++++ src/pages/admin/QRCode.jsx | 4 +- src/pages/admin/Social.jsx | 211 +++++++++++++--- 9 files changed, 1396 insertions(+), 140 deletions(-) create mode 100644 src/pages/Landing.jsx create mode 100644 src/pages/Login.jsx create mode 100644 src/pages/Signup.jsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6c17d2e..9cd921a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,8 @@ "mcp__appwrite-mcp-server__bucket_operations", "Bash(npm run build:*)", "mcp__appwrite-mcp-server__attribute_operations", - "mcp__appwrite-mcp-server__collection_operations" + "mcp__appwrite-mcp-server__collection_operations", + "mcp__appwrite-mcp-server__index_operations" ] } } diff --git a/src/App.jsx b/src/App.jsx index 91180ea..46bec92 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,12 +1,16 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { BrowserRouter, Routes, Route, Navigate, useParams } from 'react-router-dom' import { useState, useEffect } from 'react' -import { databases, DATABASE_ID, COLLECTIONS, getFilePreview } from './lib/appwrite' +import { databases, DATABASE_ID, COLLECTIONS, Query, getFilePreview } from './lib/appwrite' -// Pages +// Public Pages +import Landing from './pages/Landing' +import Signup from './pages/Signup' +import Login from './pages/Login' import Home from './pages/Home' import ComingSoon from './pages/ComingSoon' -import Setup from './pages/admin/Setup' -import Login from './pages/admin/Login' + +// Admin Pages +import AdminLogin from './pages/admin/Login' import Dashboard from './pages/admin/Dashboard' import Links from './pages/admin/Links' import Social from './pages/admin/Social' @@ -22,25 +26,22 @@ import AdminLayout from './components/AdminLayout' // Session timeout: 24 hours const SESSION_TIMEOUT = 24 * 60 * 60 * 1000 -function App() { - const [isSetupComplete, setIsSetupComplete] = useState(null) +// Component to handle slug-based profile pages +function ProfilePage() { + const { slug } = useParams() const [clientData, setClientData] = useState(null) const [loading, setLoading] = useState(true) - const [isAuthenticated, setIsAuthenticated] = useState(false) + const [notFound, setNotFound] = useState(false) useEffect(() => { - checkSetup() - checkAuth() - }, []) + loadClientBySlug() + }, [slug]) - // Update page title and favicon when client data changes useEffect(() => { if (clientData?.businessName) { document.title = clientData.businessName } - if (clientData?.logoFileId) { - // Update favicon const faviconUrl = getFilePreview(clientData.logoFileId, 64, 64) let link = document.querySelector("link[rel~='icon']") if (!link) { @@ -52,50 +53,26 @@ function App() { } }, [clientData]) - const checkAuth = () => { - const authState = sessionStorage.getItem('arklinks_auth') - const authTime = sessionStorage.getItem('arklinks_auth_time') - - if (authState === 'true' && authTime) { - const elapsed = Date.now() - parseInt(authTime) - if (elapsed < SESSION_TIMEOUT) { - setIsAuthenticated(true) - } else { - // Session expired - handleLogout() - } - } - } - - const checkSetup = async () => { + const loadClientBySlug = async () => { try { - // Check if client exists by looking for any client record - const response = await databases.listDocuments(DATABASE_ID, COLLECTIONS.CLIENTS) + const response = await databases.listDocuments( + DATABASE_ID, + COLLECTIONS.CLIENTS, + [Query.equal('slug', slug)] + ) if (response.documents.length > 0) { - const client = response.documents[0] - setClientData(client) - setIsSetupComplete(client.isSetupComplete) + setClientData(response.documents[0]) } else { - setIsSetupComplete(false) + setNotFound(true) } } catch (error) { - console.error('Setup check error:', error) - setIsSetupComplete(false) + console.error('Error loading client:', error) + setNotFound(true) } finally { setLoading(false) } } - const handleLogin = () => { - setIsAuthenticated(true) - } - - const handleLogout = () => { - sessionStorage.removeItem('arklinks_auth') - sessionStorage.removeItem('arklinks_auth_time') - setIsAuthenticated(false) - } - if (loading) { return (
@@ -104,54 +81,161 @@ function App() { ) } + if (notFound) { + return ( +
+
+

Page Not Found

+

The link page you're looking for doesn't exist.

+ + Go to Homepage + +
+
+ ) + } + + return +} + +// Component to handle admin routes for a specific client +function ClientAdmin() { + const { slug } = useParams() + const [clientData, setClientData] = useState(null) + const [loading, setLoading] = useState(true) + const [notFound, setNotFound] = useState(false) + const [isAuthenticated, setIsAuthenticated] = useState(false) + + useEffect(() => { + loadClientBySlug() + checkAuth() + }, [slug]) + + useEffect(() => { + if (clientData?.businessName) { + document.title = `Admin - ${clientData.businessName}` + } + }, [clientData]) + + const loadClientBySlug = async () => { + try { + const response = await databases.listDocuments( + DATABASE_ID, + COLLECTIONS.CLIENTS, + [Query.equal('slug', slug)] + ) + if (response.documents.length > 0) { + setClientData(response.documents[0]) + } else { + setNotFound(true) + } + } catch (error) { + console.error('Error loading client:', error) + setNotFound(true) + } finally { + setLoading(false) + } + } + + const checkAuth = () => { + const authState = sessionStorage.getItem(`arklinks_auth_${slug}`) + const authTime = sessionStorage.getItem(`arklinks_auth_time_${slug}`) + + if (authState === 'true' && authTime) { + const elapsed = Date.now() - parseInt(authTime) + if (elapsed < SESSION_TIMEOUT) { + setIsAuthenticated(true) + } else { + handleLogout() + } + } + } + + const handleLogin = () => { + sessionStorage.setItem(`arklinks_auth_${slug}`, 'true') + sessionStorage.setItem(`arklinks_auth_time_${slug}`, Date.now().toString()) + setIsAuthenticated(true) + } + + const handleLogout = () => { + sessionStorage.removeItem(`arklinks_auth_${slug}`) + sessionStorage.removeItem(`arklinks_auth_time_${slug}`) + setIsAuthenticated(false) + } + + const refreshClientData = async () => { + await loadClientBySlug() + } + + if (loading) { + return ( +
+
+
+ ) + } + + if (notFound) { + return ( +
+
+

Page Not Found

+

This admin panel doesn't exist.

+ + Go to Homepage + +
+
+ ) + } + // Check if admin password is set const hasAdminPassword = clientData?.adminPassword && clientData.adminPassword.length > 0 const needsAuth = hasAdminPassword && !isAuthenticated + if (needsAuth) { + return + } + + return ( + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ) +} + +function App() { return ( {/* Public Routes */} - : - } /> + } /> + } /> + } /> } /> - {/* Admin Login */} - - ) : ( - - ) - } /> + {/* Client Profile Page */} + } /> - {/* Admin Setup */} - { setIsSetupComplete(true); checkSetup(); }} /> - } /> - - {/* Protected Admin Routes */} - - ) : needsAuth ? ( - - ) : ( - - ) - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + {/* Client Admin Routes */} + } /> ) diff --git a/src/components/AdminLayout.jsx b/src/components/AdminLayout.jsx index 48f83a0..e0c59e1 100644 --- a/src/components/AdminLayout.jsx +++ b/src/components/AdminLayout.jsx @@ -18,30 +18,35 @@ import { import { useState } from 'react' import { getFilePreview } from '../lib/appwrite' -const navItems = [ - { path: '/admin', icon: LayoutDashboard, label: 'Dashboard', end: true }, - { path: '/admin/profile', icon: User, label: 'Profile' }, - { path: '/admin/links', icon: Link2, label: 'Links' }, - { path: '/admin/social', icon: Share2, label: 'Social Media' }, - { path: '/admin/banners', icon: Layers, label: 'Banners' }, - { path: '/admin/reviews', icon: Star, label: 'Reviews' }, - { path: '/admin/theme', icon: Palette, label: 'Theme' }, - { path: '/admin/gallery', icon: Image, label: 'Gallery' }, - { path: '/admin/apps', icon: Smartphone, label: 'Apps' }, - { path: '/admin/qr-code', icon: QrCode, label: 'QR Code' }, -] - -export default function AdminLayout({ clientData, onLogout }) { +export default function AdminLayout({ clientData, onLogout, slug }) { const [sidebarOpen, setSidebarOpen] = useState(false) const navigate = useNavigate() + // Dynamic nav items based on slug + const navItems = [ + { path: `/${slug}/admin`, icon: LayoutDashboard, label: 'Dashboard', end: true }, + { path: `/${slug}/admin/profile`, icon: User, label: 'Profile' }, + { path: `/${slug}/admin/links`, icon: Link2, label: 'Links' }, + { path: `/${slug}/admin/social`, icon: Share2, label: 'Social Media' }, + { path: `/${slug}/admin/banners`, icon: Layers, label: 'Banners' }, + { path: `/${slug}/admin/reviews`, icon: Star, label: 'Reviews' }, + { path: `/${slug}/admin/theme`, icon: Palette, label: 'Theme' }, + { path: `/${slug}/admin/gallery`, icon: Image, label: 'Gallery' }, + { path: `/${slug}/admin/apps`, icon: Smartphone, label: 'Apps' }, + { path: `/${slug}/admin/qr-code`, icon: QrCode, label: 'QR Code' }, + ] + const handleLogout = () => { if (onLogout) { onLogout() - navigate('/admin/login') + navigate('/login') } } + const handleViewLivePage = () => { + window.open(`/${slug}`, '_blank') + } + return (
{/* Mobile header */} @@ -82,7 +87,7 @@ export default function AdminLayout({ clientData, onLogout }) {

{clientData?.businessName || 'ArkLinks'}

-

Admin Panel

+

/{slug}

+ + + + {/* Submit Button */} + + + +

+ Don't have an account?{' '} + + Sign up free + +

+ + + + ) +} diff --git a/src/pages/Signup.jsx b/src/pages/Signup.jsx new file mode 100644 index 0000000..45bae4a --- /dev/null +++ b/src/pages/Signup.jsx @@ -0,0 +1,429 @@ +import { useState } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { + Link2, + ArrowRight, + Eye, + EyeOff, + Check, + Sparkles, + Building2, + AtSign, + Lock, + User +} from 'lucide-react' +import { databases, DATABASE_ID, COLLECTIONS, generateId, Query } from '../lib/appwrite' + +export default function Signup() { + const navigate = useNavigate() + const [loading, setLoading] = useState(false) + const [showPassword, setShowPassword] = useState(false) + const [error, setError] = useState('') + const [slugAvailable, setSlugAvailable] = useState(null) + const [checkingSlug, setCheckingSlug] = useState(false) + + const [formData, setFormData] = useState({ + businessName: '', + slug: '', + email: '', + password: '', + confirmPassword: '' + }) + + // Generate slug from business name + const generateSlug = (name) => { + return name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + .substring(0, 50) + } + + // Check if slug is available + const checkSlugAvailability = async (slug) => { + if (!slug || slug.length < 3) { + setSlugAvailable(null) + return + } + + setCheckingSlug(true) + try { + const response = await databases.listDocuments( + DATABASE_ID, + COLLECTIONS.CLIENTS, + [Query.equal('slug', slug)] + ) + setSlugAvailable(response.documents.length === 0) + } catch (error) { + console.error('Slug check error:', error) + setSlugAvailable(null) + } finally { + setCheckingSlug(false) + } + } + + // Handle business name change - auto-generate slug + const handleBusinessNameChange = (e) => { + const name = e.target.value + const slug = generateSlug(name) + setFormData(prev => ({ ...prev, businessName: name, slug })) + checkSlugAvailability(slug) + } + + // Handle slug change + const handleSlugChange = (e) => { + const slug = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '') + setFormData(prev => ({ ...prev, slug })) + checkSlugAvailability(slug) + } + + const handleSubmit = async (e) => { + e.preventDefault() + setError('') + + // Validation + if (formData.password !== formData.confirmPassword) { + setError('Passwords do not match') + return + } + + if (formData.password.length < 6) { + setError('Password must be at least 6 characters') + return + } + + if (!slugAvailable) { + setError('Please choose a different URL slug') + return + } + + if (formData.slug.length < 3) { + setError('URL slug must be at least 3 characters') + return + } + + setLoading(true) + + try { + // Create client record + const clientId = generateId() + await databases.createDocument( + DATABASE_ID, + COLLECTIONS.CLIENTS, + clientId, + { + businessName: formData.businessName, + slug: formData.slug, + email: formData.email || null, + adminPassword: formData.password, + isSetupComplete: true, + description: null, + phone: null, + location: null, + logoFileId: null + } + ) + + // Create default theme + await databases.createDocument( + DATABASE_ID, + COLLECTIONS.THEMES, + generateId(), + { + clientId: clientId, + primaryColor: '#3B82F6', + secondaryColor: '#1D4ED8', + backgroundColor: '#F3F4F6', + textColor: '#111827', + buttonStyle: 'rounded', + buttonColor: '#3B82F6', + fontFamily: 'Inter', + backgroundType: 'solid' + } + ) + + // Redirect to the new user's admin panel + navigate(`/${formData.slug}/admin`) + } catch (error) { + console.error('Signup error:', error) + if (error.message?.includes('slug')) { + setError('This URL is already taken. Please choose another.') + } else { + setError('Something went wrong. Please try again.') + } + } finally { + setLoading(false) + } + } + + const passwordStrength = () => { + const password = formData.password + if (!password) return { strength: 0, label: '', color: '' } + + let strength = 0 + if (password.length >= 6) strength++ + if (password.length >= 8) strength++ + if (/[A-Z]/.test(password)) strength++ + if (/[0-9]/.test(password)) strength++ + if (/[^A-Za-z0-9]/.test(password)) strength++ + + const labels = ['', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong'] + const colors = ['', 'bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-green-500', 'bg-emerald-500'] + + return { strength, label: labels[strength], color: colors[strength] } + } + + const { strength, label, color } = passwordStrength() + + return ( +
+ {/* Left Side - Branding */} +
+ {/* Background decoration */} +
+
+
+
+
+ +
+ +
+ +
+ ArkLinks + +
+ +
+
+ + Join 10,000+ creators +
+

+ Create your link page
+ + in seconds + +

+

+ Share everything you create, curate and sell from a single link in bio. +

+
+ +
+
+
+ {['A', 'B', 'C', 'D'].map((letter, i) => ( +
+ {letter} +
+ ))} +
+

+ 1,000+ signed up this week +

+
+
+
+ + {/* Right Side - Form */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+ ArkLinks + +
+ +
+
+

Create your account

+

Start building your link page today

+
+ + {error && ( +
+ {error} +
+ )} + +
+ {/* Business Name */} +
+ +
+ + +
+
+ + {/* Slug / URL */} +
+ +
+ + links.arkylx.com/ + + +
+ {checkingSlug ? ( +
+ ) : slugAvailable === true ? ( + + ) : slugAvailable === false ? ( + Taken + ) : null} +
+
+ {formData.slug && slugAvailable === true && ( +

+ + This URL is available! +

+ )} +
+ + {/* Email */} +
+ +
+ + setFormData(prev => ({ ...prev, email: e.target.value }))} + className="w-full pl-12 pr-4 py-3.5 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all bg-gray-50 focus:bg-white" + placeholder="you@example.com" + /> +
+
+ + {/* Password */} +
+ +
+ + setFormData(prev => ({ ...prev, password: e.target.value }))} + className="w-full pl-12 pr-12 py-3.5 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all bg-gray-50 focus:bg-white" + placeholder="Create a password" + /> + +
+ {formData.password && ( +
+
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ ))} +
+

{label}

+
+ )} +
+ + {/* Confirm Password */} +
+ +
+ + setFormData(prev => ({ ...prev, confirmPassword: e.target.value }))} + className={`w-full pl-12 pr-12 py-3.5 border rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all bg-gray-50 focus:bg-white ${ + formData.confirmPassword && formData.password !== formData.confirmPassword + ? 'border-red-300' + : 'border-gray-200' + }`} + placeholder="Confirm your password" + /> + {formData.confirmPassword && formData.password === formData.confirmPassword && ( + + )} +
+
+ + {/* Submit Button */} + + + +

+ Already have an account?{' '} + + Log in + +

+
+ +

+ By signing up, you agree to our Terms of Service and Privacy Policy +

+
+
+
+ ) +} diff --git a/src/pages/admin/QRCode.jsx b/src/pages/admin/QRCode.jsx index 74c3970..6d0e9cc 100644 --- a/src/pages/admin/QRCode.jsx +++ b/src/pages/admin/QRCode.jsx @@ -3,7 +3,7 @@ import { QRCodeSVG } from 'qrcode.react' import { Download, Copy, Check } from 'lucide-react' import { getFilePreview } from '../../lib/appwrite' -export default function QRCode({ clientData }) { +export default function QRCode({ clientData, slug }) { const qrRef = useRef(null) const [copied, setCopied] = useState(false) const [settings, setSettings] = useState({ @@ -14,7 +14,7 @@ export default function QRCode({ clientData }) { includeLogo: true }) - const pageUrl = window.location.origin + const pageUrl = `${window.location.origin}/${slug}` const downloadQR = () => { const svg = qrRef.current?.querySelector('svg') diff --git a/src/pages/admin/Social.jsx b/src/pages/admin/Social.jsx index a612f8b..b0c52a4 100644 --- a/src/pages/admin/Social.jsx +++ b/src/pages/admin/Social.jsx @@ -1,10 +1,10 @@ import { useState, useEffect } from 'react' -import { Plus, Edit2, Trash2, GripVertical } from 'lucide-react' +import { Plus, Edit2, Trash2, GripVertical, Upload, X } from 'lucide-react' import { Facebook, Twitter, Instagram, Linkedin, Youtube, Github, - MessageCircle, Send, Music2, Globe + MessageCircle, Send, Music2, Globe, Sparkles } from 'lucide-react' -import { databases, DATABASE_ID, COLLECTIONS, generateId, Query } from '../../lib/appwrite' +import { databases, DATABASE_ID, COLLECTIONS, generateId, Query, uploadFile, deleteFile, getFilePreview } from '../../lib/appwrite' const PLATFORMS = [ { id: 'facebook', name: 'Facebook', icon: Facebook, color: '#1877F2' }, @@ -17,17 +17,23 @@ const PLATFORMS = [ { id: 'whatsapp', name: 'WhatsApp', icon: MessageCircle, color: '#25D366' }, { id: 'telegram', name: 'Telegram', icon: Send, color: '#26A5E4' }, { id: 'website', name: 'Website', icon: Globe, color: '#4B5563' }, + { id: 'custom', name: 'Custom', icon: Sparkles, color: '#8B5CF6' }, ] export default function Social({ clientData }) { const [socialLinks, setSocialLinks] = useState([]) const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) const [showModal, setShowModal] = useState(false) const [editingLink, setEditingLink] = useState(null) + const [iconPreview, setIconPreview] = useState(null) const [formData, setFormData] = useState({ platform: 'facebook', url: '', - isEnabled: true + isEnabled: true, + customName: '', + customIconFile: null, + customIconFileId: '' }) useEffect(() => { @@ -60,35 +66,87 @@ export default function Social({ clientData }) { setFormData({ platform: link.platform, url: link.url, - isEnabled: link.isEnabled ?? true + isEnabled: link.isEnabled ?? true, + customName: link.customName || '', + customIconFile: null, + customIconFileId: link.customIconFileId || '' }) + if (link.customIconFileId) { + setIconPreview(getFilePreview(link.customIconFileId, 100, 100)) + } else { + setIconPreview(null) + } } else { setEditingLink(null) setFormData({ platform: 'facebook', url: '', - isEnabled: true + isEnabled: true, + customName: '', + customIconFile: null, + customIconFileId: '' }) + setIconPreview(null) } setShowModal(true) } + const handleIconChange = (e) => { + const file = e.target.files[0] + if (file) { + setFormData(prev => ({ ...prev, customIconFile: file })) + setIconPreview(URL.createObjectURL(file)) + } + } + + const removeIcon = () => { + setFormData(prev => ({ ...prev, customIconFile: null, customIconFileId: '' })) + setIconPreview(null) + } + const handleSubmit = async (e) => { e.preventDefault() + setSaving(true) + try { + let customIconFileId = formData.customIconFileId + + // Upload new custom icon if provided + if (formData.customIconFile) { + // Delete old icon if exists + if (editingLink?.customIconFileId) { + try { + await deleteFile(editingLink.customIconFileId) + } catch (err) { + console.log('Could not delete old icon') + } + } + customIconFileId = await uploadFile(formData.customIconFile) + } else if (!formData.customIconFileId && editingLink?.customIconFileId) { + // Icon was removed + try { + await deleteFile(editingLink.customIconFileId) + } catch (err) { + console.log('Could not delete old icon') + } + customIconFileId = null + } + + const data = { + platform: formData.platform, + url: formData.url, + isEnabled: formData.isEnabled, + customName: formData.platform === 'custom' ? formData.customName : null, + customIconFileId: formData.platform === 'custom' ? customIconFileId : null + } + if (editingLink) { - await databases.updateDocument(DATABASE_ID, COLLECTIONS.SOCIAL_LINKS, editingLink.$id, { - platform: formData.platform, - url: formData.url, - isEnabled: formData.isEnabled - }) + await databases.updateDocument(DATABASE_ID, COLLECTIONS.SOCIAL_LINKS, editingLink.$id, data) } else { await databases.createDocument(DATABASE_ID, COLLECTIONS.SOCIAL_LINKS, generateId(), { + ...data, clientId: clientData.$id, - platform: formData.platform, - url: formData.url, - order: socialLinks.length, - isEnabled: formData.isEnabled + order: socialLinks.length }) } setShowModal(false) @@ -96,13 +154,23 @@ export default function Social({ clientData }) { } catch (error) { console.error('Error saving social link:', error) alert('Error saving social link') + } finally { + setSaving(false) } } - const deleteLink = async (linkId) => { + const deleteLink = async (link) => { if (!confirm('Are you sure you want to delete this social link?')) return try { - await databases.deleteDocument(DATABASE_ID, COLLECTIONS.SOCIAL_LINKS, linkId) + // Delete custom icon if exists + if (link.customIconFileId) { + try { + await deleteFile(link.customIconFileId) + } catch (err) { + console.log('Could not delete icon') + } + } + await databases.deleteDocument(DATABASE_ID, COLLECTIONS.SOCIAL_LINKS, link.$id) loadSocialLinks() } catch (error) { console.error('Error deleting social link:', error) @@ -161,6 +229,8 @@ export default function Social({ clientData }) { {socialLinks.map((link) => { const platform = getPlatformInfo(link.platform) const Icon = platform.icon + const isCustom = link.platform === 'custom' + const displayName = isCustom && link.customName ? link.customName : platform.name return (
@@ -168,15 +238,26 @@ export default function Social({ clientData }) {
-
- -
+ {isCustom && link.customIconFileId ? ( + {displayName} + ) : ( +
+ +
+ )}
-

{platform.name}

+

+ {displayName} + {isCustom && Custom} +

{link.url}

@@ -200,7 +281,7 @@ export default function Social({ clientData }) { ) })}
+ {/* Custom platform options */} + {formData.platform === 'custom' && ( + <> +
+ + setFormData(prev => ({ ...prev, customName: e.target.value }))} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="e.g., Discord, Threads, Snapchat" + /> +
+ +
+ + {iconPreview ? ( +
+ Custom icon preview +
+

Icon uploaded

+ +
+
+ ) : ( + + )} +
+ + )} +
setFormData(prev => ({ ...prev, url: e.target.value }))} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="https://facebook.com/yourpage" + placeholder={formData.platform === 'custom' ? 'https://example.com/yourprofile' : `https://${formData.platform}.com/yourpage`} />
@@ -277,9 +417,14 @@ export default function Social({ clientData }) {