commit 70aa1ad46ee53b4b6ac99cf5cc7f152822dbdc14 Author: Ryan Salazar Date: Thu May 7 16:39:17 2026 +0000 initial commit: rocket.new export of broadcastbeat diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..385c288 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,57 @@ +{ + "root": true, + "extends": [ + "next/core-web-vitals", + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint", + "prettier" + ], + "rules": { + "prettier/prettier": [ + "error", + { + "endOfLine": "auto", + "singleQuote": true, + "semi": true, + "tabWidth": 2, + "printWidth": 100, + "trailingComma": "es5" + } + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-explicit-any": "warn", + "no-console": [ + "warn", + { + "allow": [ + "warn", + "error", + "info" + ] + } + ] + }, + "ignorePatterns": [ + "node_modules/", + ".next/", + "out/", + "public/" + ], + "env": { + "browser": true, + "node": true, + "es6": true + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5211b43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Node modules +node_modules/ +package-lock.json + +# Next.js build output +.next/ +out/ + +# Production build +build/ + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# OS-specific files +.DS_Store +Thumbs.db + +# Vercel deployment +.vercel/ + +# Environment files (secrets — push to Coolify as app env vars instead) +.env +.env.local +.env.*.local + +# Optional: if using TypeScript +*.tsbuildinfo + +# Optional: if using testing tools +coverage/ +jest-test-results.json + +# Optional: if using IDEs +.vscode/ +.idea/ + +# Optional: if using TurboRepo or monorepo +turbo/ \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..3d3333b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +# .prettierignore +build +coverage +node_modules +public diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..3d14104 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "tabWidth": 2, + "printWidth": 100, + "singleQuote": true, + "trailingComma": "es5", + "bracketSameLine": false, + "endOfLine": "auto" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..c699dfd --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# Next.js + +A modern Next.js 15 application built with TypeScript and Tailwind CSS. + +## 🚀 Features + +- **Next.js 15** - Latest version with improved performance and features +- **React 19** - Latest React version with enhanced capabilities +- **Tailwind CSS** - Utility-first CSS framework for rapid UI development + +## 🛠️ Installation + +1. Install dependencies: + ```bash + npm install + # or + yarn install + ``` + +2. Start the development server: + ```bash + npm run dev + # or + yarn dev + ``` +3. Open [http://localhost:4028](http://localhost:4028) with your browser to see the result. + +## 📁 Project Structure + +``` +nextjs/ +├── public/ # Static assets +├── src/ +│ ├── app/ # App router components +│ │ ├── layout.tsx # Root layout component +│ │ └── page.tsx # Main page component +│ ├── components/ # Reusable UI components +│ ├── styles/ # Global styles and Tailwind configuration +├── next.config.mjs # Next.js configuration +├── package.json # Project dependencies and scripts +├── postcss.config.js # PostCSS configuration +└── tailwind.config.js # Tailwind CSS configuration + +``` + +## 🧩 Page Editing + +You can start editing the page by modifying `src/app/page.tsx`. The page auto-updates as you edit the file. + +## 🎨 Styling + +This project uses Tailwind CSS for styling with the following features: +- Utility-first approach for rapid development +- Custom theme configuration +- Responsive design utilities +- PostCSS and Autoprefixer integration + +## 📦 Available Scripts + +- `npm run dev` - Start development server on port 4028 +- `npm run build` - Build the application for production +- `npm run start` - Start the development server +- `npm run serve` - Start the production server +- `npm run lint` - Run ESLint to check code quality +- `npm run lint:fix` - Fix ESLint issues automatically +- `npm run format` - Format code with Prettier + +## 📱 Deployment + +Build the application for production: + + ```bash + npm run build + ``` + +## 📚 Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial + +You can check out the [Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## 🙏 Acknowledgments + +- Built with [Rocket.new](https://rocket.new) +- Powered by Next.js and React +- Styled with Tailwind CSS + +Built with ❤️ on Rocket.new \ No newline at end of file diff --git a/config/ai-nodes.json b/config/ai-nodes.json new file mode 100644 index 0000000..f53ee87 --- /dev/null +++ b/config/ai-nodes.json @@ -0,0 +1,26 @@ +{ + "nodes": [ + { + "name": "AI_001", + "host": "68.248.192.64", + "port": 8765, + "api_key_env": "AI_NODE_API_KEY", + "models": [ + "llama3.1:70b", + "qwen2.5-coder:32b", + "deepseek-r1" + ] + }, + { + "name": "AI_002", + "host": "68.248.192.64", + "port": 8766, + "api_key_env": "AI_NODE_API_KEY", + "models": [ + "llama3.1:70b", + "qwen2.5-coder:32b", + "deepseek-r1" + ] + } + ] +} \ No newline at end of file diff --git a/image-hosts.config.js b/image-hosts.config.js new file mode 100644 index 0000000..579591e --- /dev/null +++ b/image-hosts.config.js @@ -0,0 +1,34 @@ +/** + * Image Hosts Configuration (add your image hosts here) + */ + +export const imageHosts = [ + { + protocol: 'https', + hostname: 'images.unsplash.com', + }, + { + protocol: 'https', + hostname: 'images.pexels.com', + }, + { + protocol: 'https', + hostname: 'images.pixabay.com', + }, + { + protocol: 'https', + hostname: 'img.rocket.new', + }, + { + protocol: 'https', + hostname: '*.supabase.co', + }, + { + protocol: 'https', + hostname: '*.supabase.in', + }, + { + protocol: 'https', + hostname: 'www.broadcastbeat.com', + }, +]; diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..6ef1a95 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,133 @@ +import { imageHosts } from './image-hosts.config.js'; + +/** @type {import('next').NextConfig} */ +const nextConfig = { + productionBrowserSourceMaps: true, + distDir: process.env.DIST_DIR || '.next', + + typescript: { + ignoreBuildErrors: true, + }, + + eslint: { + ignoreDuringBuilds: true, + }, + + images: { + remotePatterns: imageHosts, + formats: ['image/avif', 'image/webp'], + minimumCacheTTL: 86400, + deviceSizes: [640, 750, 828, 1080, 1200, 1920], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], + }, + + // Performance: compress responses + compress: true, + + // Performance: power headers for caching static assets + async headers() { + return [ + { + source: '/assets/:path*', + headers: [ + { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }, + ], + }, + { + source: '/_next/static/:path*', + headers: [ + { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }, + ], + }, + { + source: '/_next/image/:path*', + headers: [ + { key: 'Cache-Control', value: 'public, max-age=86400, stale-while-revalidate=604800' }, + ], + }, + ]; + }, + + async redirects() { + return [ + // Root redirect + { + source: '/', + destination: '/home-page', + permanent: false, + }, + // WordPress admin redirects for broadcastbeat.com (301 permanent) + { + source: '/wp-admin', + destination: '/admin', + permanent: true, + }, + { + source: '/wp-admin/', + destination: '/admin', + permanent: true, + }, + { + source: '/wp-admin/post-new.php', + destination: '/contributor', + permanent: true, + }, + { + source: '/wp-admin/edit.php', + destination: '/admin/articles', + permanent: true, + }, + { + source: '/wp-admin/profile.php', + destination: '/account', + permanent: true, + }, + { + source: '/wp-login.php', + destination: '/login', + permanent: true, + }, + { + source: '/wp-login.php/:path*', + destination: '/login', + permanent: true, + }, + // Catch-all for any other wp-admin sub-paths + { + source: '/wp-admin/:path*', + destination: '/admin', + permanent: true, + }, + // AV Beat dashboard redirect (avbeat.com/wp-admin/ → unified dashboard) + // These are handled by the same Next.js app when serving avbeat.com + { + source: '/avbeat/wp-admin', + destination: '/admin', + permanent: true, + }, + { + source: '/avbeat/wp-admin/', + destination: '/admin', + permanent: true, + }, + { + source: '/avbeat/wp-login.php', + destination: '/login', + permanent: true, + }, + { + source: '/avbeat/wp-login.php/:path*', + destination: '/login', + permanent: true, + }, + // Login aliases + { + source: '/login', + destination: '/login', + permanent: false, + }, + ]; + } +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..53765db --- /dev/null +++ b/package.json @@ -0,0 +1,96 @@ +{ + "name": "broadcastbeat", + "version": "0.1.0", + "private": true, + "rocketCritical": { + "dependencies": [ + "next", + "react", + "react-dom", + "@dhiwise/component-tagger", + "@tailwindcss/typography", + "recharts" + ], + "devDependencies": [ + "typescript", + "@types/node", + "@types/react", + "@types/react-dom", + "@typescript-eslint/eslint-plugin", + "@typescript-eslint/parser", + "eslint-config-next", + "tailwindcss", + "autoprefixer", + "postcss", + "eslint", + "prettier", + "@netlify/plugin-nextjs" + ], + "scripts": [ + "dev", + "build", + "start", + "lint", + "lint:fix", + "format", + "serve", + "type-check" + ], + "warning": "🚨 CRITICAL: DO NOT REMOVE OR MODIFY ABOVE DEPENDENCIES - Required for Next.js 15 TypeScript app functionality" + }, + "scripts": { + "dev": "next dev -p 4028", + "build": "next build", + "start": "next dev -p 4028", + "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,css,md,json}\"", + "serve": "next start", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@dhiwise/component-tagger": "^1.0.14", + "@heroicons/react": "^2.2.0", + "@rocketnew/llm-sdk": "^1.0.3", + "@supabase/ssr": "^0.6.1", + "@supabase/supabase-js": "^2.49.4", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.16", + "@tiptap/extension-color": "3.20.4", + "@tiptap/extension-image": "3.20.4", + "@tiptap/extension-link": "3.20.4", + "@tiptap/extension-placeholder": "3.20.4", + "@tiptap/extension-text-align": "3.20.4", + "@tiptap/extension-text-style": "3.20.4", + "@tiptap/extension-underline": "3.20.4", + "@tiptap/react": "3.20.4", + "@tiptap/starter-kit": "3.20.4", + "bcryptjs": "3.0.3", + "next": "15.1.11", + "nodemailer": "8.0.4", + "react": "19.0.3", + "react-dom": "19.0.3", + "recharts": "^2.15.2" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@netlify/plugin-nextjs": "^5.11.1", + "@types/bcryptjs": "2.4.6", + "@types/node": "^20", + "@types/nodemailer": "^6.4.17", + "@types/react": "19.0.3", + "@types/react-dom": "19.0.3", + "@typescript-eslint/eslint-plugin": "^8.29.0", + "@typescript-eslint/parser": "^8.29.0", + "autoprefixer": "10.4.2", + "eslint": "^9", + "eslint-config-next": "^15.2.4", + "eslint-config-prettier": "^10.1.1", + "eslint-plugin-prettier": "^5.2.6", + "postcss": "8.4.8", + "prettier": "^3.5.3", + "tailwindcss": "3.4.6", + "tailwindcss-animate": "^1.0.7", + "typescript": "^5" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..96bb01e --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/public/assets/images/no_image.png b/public/assets/images/no_image.png new file mode 100644 index 0000000..b42dd2f Binary files /dev/null and b/public/assets/images/no_image.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..4eb44dd Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx new file mode 100644 index 0000000..9dd4a97 --- /dev/null +++ b/src/app/about/page.tsx @@ -0,0 +1,123 @@ +import React from "react"; +import type { Metadata } from "next"; +import Link from "next/link"; +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; + +export const metadata: Metadata = { + title: "About BroadcastBeat — The Digital Platform for Broadcast Engineering", + description: + "BroadcastBeat is the leading digital platform for broadcast engineering professionals. Learn about our editorial mission, team, and commitment to the broadcast industry.", + alternates: { canonical: "/about" }, + openGraph: { + title: "About BroadcastBeat", + description: "BroadcastBeat is the leading digital platform for broadcast engineering professionals.", + url: "/about", + type: "website", + }, +}; + +const team = [ + { name: "Elena Vasquez", title: "Features Editor", bio: "Elena covers the intersection of technology and storytelling in broadcast media, with a focus on emerging production workflows and industry trends." }, + { name: "James Whitfield", title: "Senior Technology Correspondent", bio: "James has covered broadcast technology for over a decade, specializing in IP infrastructure, streaming, and the business of broadcast." }, + { name: "Sarah Chen", title: "Audio Technology Editor", bio: "Sarah brings deep expertise in professional audio to her coverage of mixing consoles, audio networking, and live production sound." }, + { name: "Rachel Kim", title: "Technology Analyst", bio: "Rachel focuses on data-driven coverage of industry trends, market analysis, and the business implications of new broadcast technologies." }, + { name: "Marcus Rivera", title: "Sports Technology Correspondent", bio: "Marcus covers the intersection of sports and broadcast technology, from remote production to interactive fan experiences." }, +]; + +export default function AboutPage() { + return ( +
+
+ + {/* DO NOT OVERRIDE — About page hero */} +
+
+
+ About +
+
+

About BroadcastBeat

+
+
+ +
+
+
+ {/* Mission */} +
+

Our Mission

+
+

+ BroadcastBeat is the digital platform of record for broadcast engineering professionals. We cover the technology, people, and business decisions that shape how the world's media is created, distributed, and consumed. +

+

+ From breaking news about the latest IP infrastructure deployments to in-depth reviews of broadcast cameras and audio consoles, BroadcastBeat provides the coverage that broadcast engineers, producers, and executives rely on to stay ahead of an industry in constant transformation. +

+

+ We are an official media partner of NAB Show and provide comprehensive coverage of all major broadcast industry events, including IBC, Cine Gear, and the Streaming Summit. +

+
+
+ + {/* Team */} +
+

Editorial Team

+
+ {team.map((member) => ( +
+
+ {member.name[0]} +
+
+

{member.name}

+

{member.title}

+

{member.bio}

+
+
+ ))} +
+
+ + {/* Contact */} +
+

Contact

+
+
+

Editorial

+ editorial@broadcastbeat.com +
+
+

Advertising Sales

+ jeff@broadcastbeat.com +
+
+

Press & Media

+ press@broadcastbeat.com +
+
+
+
+ + +
+
+
+
+ ); +} diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx new file mode 100644 index 0000000..eb79d0b --- /dev/null +++ b/src/app/account/page.tsx @@ -0,0 +1,842 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import Link from "next/link"; +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; +import AppImage from "@/components/ui/AppImage"; +import { createClient } from "@/lib/supabase/client"; +import { useRouter } from "next/navigation"; + +interface UserProfile { + id: string; + email: string; + full_name: string; + avatar_url: string | null; + bio: string | null; + website: string | null; + location: string | null; + reputation: number; + created_at: string; +} + +interface BookmarkItem { + id: string; + article_slug: string; + article_title: string; + article_excerpt: string | null; + article_image: string | null; + article_image_alt: string | null; + article_category: string | null; + article_author: string | null; + article_read_time: string | null; + article_date: string | null; + saved_at: string; +} + +interface HistoryItem { + id: string; + article_slug: string; + article_title: string; + article_excerpt: string | null; + article_image: string | null; + article_image_alt: string | null; + article_category: string | null; + article_author: string | null; + article_read_time: string | null; + article_date: string | null; + viewed_at: string; +} + +interface TopicPref { + id: string; + topic: string; + view_count: number; + last_viewed_at: string; +} + +type ActiveTab = "profile" | "security" | "bookmarks" | "history" | "topics" | "preferences"; + +export default function AccountPage() { + const router = useRouter(); + const supabase = createClient(); + + const [user, setUser] = useState(null); + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [activeTab, setActiveTab] = useState("profile"); + const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); + + // Profile form state + const [fullName, setFullName] = useState(""); + const [bio, setBio] = useState(""); + const [website, setWebsite] = useState(""); + const [location, setLocation] = useState(""); + + // Security form state + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [changingPassword, setChangingPassword] = useState(false); + + // Digest preference state + const [weeklyDigest, setWeeklyDigest] = useState(false); + const [savingDigest, setSavingDigest] = useState(false); + + // Bookmarks + const [bookmarks, setBookmarks] = useState([]); + const [bookmarksLoading, setBookmarksLoading] = useState(false); + + // Reading History + const [history, setHistory] = useState([]); + const [historyLoading, setHistoryLoading] = useState(false); + + // Topic Preferences + const [topics, setTopics] = useState([]); + const [topicsLoading, setTopicsLoading] = useState(false); + + const showToast = (message: string, type: "success" | "error") => { + setToast({ message, type }); + setTimeout(() => setToast(null), 3500); + }; + + useEffect(() => { + const init = async () => { + const { data: { user: authUser } } = await supabase.auth.getUser(); + if (!authUser) { + router.push("/login"); + return; + } + setUser(authUser); + + const { data: profileData } = await supabase + .from("user_profiles") + .select("*") + .eq("id", authUser.id) + .maybeSingle(); + + if (profileData) { + setProfile(profileData); + setFullName(profileData.full_name || ""); + setBio(profileData.bio || ""); + setWebsite(profileData.website || ""); + setLocation(profileData.location || ""); + } + setLoading(false); + }; + init(); + }, []); + + useEffect(() => { + if (!user) return; + fetch("/api/digest/preferences") + .then((r) => r.json()) + .then((d) => setWeeklyDigest(d.weekly_digest_enabled ?? false)) + .catch(() => {}); + }, [user]); + + // Load bookmarks when tab is active + useEffect(() => { + if (activeTab !== "bookmarks" || !user) return; + setBookmarksLoading(true); + supabase + .from("reading_list") + .select("*") + .eq("user_id", user.id) + .order("saved_at", { ascending: false }) + .then(({ data }) => { + setBookmarks(data || []); + setBookmarksLoading(false); + }); + }, [activeTab, user]); + + // Load reading history when tab is active + useEffect(() => { + if (activeTab !== "history" || !user) return; + setHistoryLoading(true); + supabase + .from("reading_history") + .select("*") + .eq("user_id", user.id) + .order("viewed_at", { ascending: false }) + .limit(50) + .then(({ data }) => { + setHistory(data || []); + setHistoryLoading(false); + }); + }, [activeTab, user]); + + // Load topic preferences when tab is active + useEffect(() => { + if (activeTab !== "topics" || !user) return; + setTopicsLoading(true); + supabase + .from("user_topic_preferences") + .select("*") + .eq("user_id", user.id) + .order("view_count", { ascending: false }) + .then(({ data }) => { + setTopics(data || []); + setTopicsLoading(false); + }); + }, [activeTab, user]); + + const handleSaveProfile = async (e: React.FormEvent) => { + e.preventDefault(); + if (!user) return; + setSaving(true); + try { + const { error } = await supabase + .from("user_profiles") + .upsert({ + id: user.id, + email: user.email, + full_name: fullName, + bio, + website, + location, + }); + if (error) throw error; + showToast("Profile updated successfully", "success"); + } catch (err: any) { + showToast(err?.message || "Failed to update profile", "error"); + } finally { + setSaving(false); + } + }; + + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + if (newPassword !== confirmPassword) { + showToast("Passwords do not match", "error"); + return; + } + if (newPassword.length < 6) { + showToast("Password must be at least 6 characters", "error"); + return; + } + setChangingPassword(true); + try { + const { error } = await supabase.auth.updateUser({ password: newPassword }); + if (error) throw error; + setNewPassword(""); + setConfirmPassword(""); + showToast("Password updated successfully", "success"); + } catch (err: any) { + showToast(err?.message || "Failed to update password", "error"); + } finally { + setChangingPassword(false); + } + }; + + const handleSignOut = async () => { + await supabase.auth.signOut(); + router.push("/home-page"); + }; + + const handleToggleDigest = async (enabled: boolean) => { + setSavingDigest(true); + try { + const res = await fetch("/api/digest/preferences", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ weekly_digest_enabled: enabled }), + }); + if (!res.ok) throw new Error("Failed to update"); + setWeeklyDigest(enabled); + showToast(enabled ? "Weekly digest enabled" : "Weekly digest disabled", "success"); + } catch { + showToast("Failed to update digest preference", "error"); + } finally { + setSavingDigest(false); + } + }; + + const handleRemoveBookmark = async (id: string, slug: string) => { + if (!user) return; + try { + await supabase.from("reading_list").delete().eq("id", id).eq("user_id", user.id); + setBookmarks((prev) => prev.filter((b) => b.id !== id)); + showToast("Bookmark removed", "success"); + } catch { + showToast("Failed to remove bookmark", "error"); + } + }; + + const handleClearHistory = async () => { + if (!user) return; + try { + await supabase.from("reading_history").delete().eq("user_id", user.id); + setHistory([]); + showToast("Reading history cleared", "success"); + } catch { + showToast("Failed to clear history", "error"); + } + }; + + const tabs: { id: ActiveTab; label: string }[] = [ + { id: "profile", label: "Profile" }, + { id: "security", label: "Security" }, + { id: "bookmarks", label: "Bookmarks" }, + { id: "history", label: "History" }, + { id: "topics", label: "Topics" }, + { id: "preferences", label: "Preferences" }, + ]; + + if (loading) { + return ( +
+
+
+
+
+
+
+
+
+
+ ); + } + + return ( +
+
+ + {/* Toast */} + {toast && ( +
+ {toast.type === "success" ? ( + + ) : ( + + )} + {toast.message} +
+ )} + + {/* Breadcrumb */} +
+
+ +
+
+ +
+
+ {/* Page Header */} +
+
+

Account Settings

+

{user?.email}

+
+ +
+ + {/* Tabs */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* Profile Tab */} + {activeTab === "profile" && ( +
+
+

+ Public Profile +

+
+
+ + setFullName(e.target.value)} + placeholder="Your full name" + className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]" + /> +
+
+ +