From 507b80430dd356bc81f740084216d35ad62a9d2e Mon Sep 17 00:00:00 2001 From: Coder-soft Date: Wed, 17 Dec 2025 21:55:24 +0500 Subject: [PATCH 1/4] Holoucast Update --- package-lock.json | 81 +-- package.json | 2 + pnpm-lock.yaml | 17 + src/App.tsx | 38 +- src/components/Footer.tsx | 72 ++- src/components/Navbar.tsx | 257 +++----- src/components/admin/AdminBlogsManager.tsx | 148 +++++ .../admin/AdminResourcesManager.tsx | 16 +- src/components/admin/BlogEditor.tsx | 171 +++++ src/components/auth/UserMenu.tsx | 6 + src/components/profile/ProfileEditor.tsx | 583 ++++++++++++++++++ src/components/profile/ProfileThemeEngine.tsx | 75 +++ src/components/profile/SvglPicker.tsx | 178 ++++++ src/components/resources/ResourceCard.tsx | 426 +++++++------ src/components/ui/alert-dialog.tsx | 2 +- src/components/ui/badge-variants.ts | 23 + src/components/ui/badge.tsx | 26 +- src/components/ui/button-variants.ts | 30 + src/components/ui/button.tsx | 32 +- src/components/ui/calendar.tsx | 2 +- src/components/ui/command.tsx | 2 +- src/components/ui/form.tsx | 49 +- src/components/ui/navigation-menu-variants.ts | 5 + src/components/ui/navigation-menu.tsx | 9 +- src/components/ui/pagination.tsx | 3 +- src/components/ui/sidebar-context.tsx | 29 + src/components/ui/sidebar.tsx | 41 +- src/components/ui/textarea.tsx | 3 +- src/components/ui/toggle-variants.ts | 25 + src/components/ui/toggle.tsx | 28 +- src/components/ui/use-form-field.ts | 44 ++ src/hooks/useAuth.tsx | 264 +------- src/hooks/useProfile.ts | 24 +- src/hooks/useResources.ts | 492 +++++++-------- src/hooks/useUserFavorites.ts | 80 ++- src/index.css | 36 +- src/integrations/supabase/types.ts | 151 +++-- src/lib/showcase.ts | 20 +- src/lib/showcases.ts | 4 +- src/lib/utils.ts | 46 ++ src/main.tsx | 2 + src/pages/Admin.tsx | 16 +- src/pages/AiTitleHelper.tsx | 5 +- src/pages/BlogView.tsx | 155 +++++ src/pages/Blogs.tsx | 163 +++++ src/pages/GuideView.tsx | 5 +- src/pages/Index.tsx | 6 +- src/pages/NotFound.tsx | 2 +- src/pages/Profile.tsx | 333 +++++----- src/pages/Showcase.tsx | 80 +-- src/pages/YouTubeDownloader.tsx | 14 +- src/providers/AuthContext.tsx | 33 + src/providers/AuthProvider.tsx | 230 +++++++ src/types/profile.ts | 83 +++ src/types/resources.ts | 3 +- ...251214152600_add_profile_customization.sql | 6 + tailwind.config.ts | 6 +- test.md | 0 ....timestamp-1765978667252-2d9e972de6409.mjs | 62 ++ 59 files changed, 3214 insertions(+), 1530 deletions(-) create mode 100644 src/components/admin/AdminBlogsManager.tsx create mode 100644 src/components/admin/BlogEditor.tsx create mode 100644 src/components/profile/ProfileEditor.tsx create mode 100644 src/components/profile/ProfileThemeEngine.tsx create mode 100644 src/components/profile/SvglPicker.tsx create mode 100644 src/components/ui/badge-variants.ts create mode 100644 src/components/ui/button-variants.ts create mode 100644 src/components/ui/navigation-menu-variants.ts create mode 100644 src/components/ui/sidebar-context.tsx create mode 100644 src/components/ui/toggle-variants.ts create mode 100644 src/components/ui/use-form-field.ts create mode 100644 src/pages/BlogView.tsx create mode 100644 src/pages/Blogs.tsx create mode 100644 src/providers/AuthContext.tsx create mode 100644 src/providers/AuthProvider.tsx create mode 100644 src/types/profile.ts create mode 100644 supabase/migrations/20251214152600_add_profile_customization.sql create mode 100644 test.md create mode 100644 vite.config.ts.timestamp-1765978667252-2d9e972de6409.mjs diff --git a/package-lock.json b/package-lock.json index 11d6dc9..93637e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,7 +83,7 @@ "js-cookie": "^3.0.5", "kbar": "^0.1.0-beta.46", "motion": "^12.18.1", - "next": "^15.3.2", + "next": "15.3.6", "next-themes": "^0.3.0", "openai": "^4.103.0", "pixelarticons": "^1.8.1", @@ -1715,15 +1715,15 @@ ] }, "node_modules/@next/env": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.4.tgz", - "integrity": "sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==", + "version": "15.3.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.6.tgz", + "integrity": "sha512-/cK+QPcfRbDZxmI/uckT4lu9pHCfRIPBLqy88MhE+7Vg5hKrEYc333Ae76dn/cw2FBP2bR/GoK/4DU+U7by/Nw==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.4.tgz", - "integrity": "sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.5.tgz", + "integrity": "sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==", "cpu": [ "arm64" ], @@ -1737,9 +1737,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.4.tgz", - "integrity": "sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.5.tgz", + "integrity": "sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==", "cpu": [ "x64" ], @@ -1753,9 +1753,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.4.tgz", - "integrity": "sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.5.tgz", + "integrity": "sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==", "cpu": [ "arm64" ], @@ -1769,9 +1769,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.4.tgz", - "integrity": "sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.5.tgz", + "integrity": "sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==", "cpu": [ "arm64" ], @@ -1785,9 +1785,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.4.tgz", - "integrity": "sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.5.tgz", + "integrity": "sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==", "cpu": [ "x64" ], @@ -1801,9 +1801,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.4.tgz", - "integrity": "sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.5.tgz", + "integrity": "sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==", "cpu": [ "x64" ], @@ -1817,9 +1817,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.4.tgz", - "integrity": "sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.5.tgz", + "integrity": "sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==", "cpu": [ "arm64" ], @@ -1833,9 +1833,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.4.tgz", - "integrity": "sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.5.tgz", + "integrity": "sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==", "cpu": [ "x64" ], @@ -9581,12 +9581,13 @@ } }, "node_modules/next": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/next/-/next-15.3.4.tgz", - "integrity": "sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==", + "version": "15.3.6", + "resolved": "https://registry.npmjs.org/next/-/next-15.3.6.tgz", + "integrity": "sha512-oI6D1zbbsh6JzzZFDCSHnnx6Qpvd1fSkVJu/5d8uluqnxzuoqtodVZjYvNovooznUq8udSAiKp7MbwlfZ8Gm6w==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", "license": "MIT", "dependencies": { - "@next/env": "15.3.4", + "@next/env": "15.3.6", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -9601,14 +9602,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.3.4", - "@next/swc-darwin-x64": "15.3.4", - "@next/swc-linux-arm64-gnu": "15.3.4", - "@next/swc-linux-arm64-musl": "15.3.4", - "@next/swc-linux-x64-gnu": "15.3.4", - "@next/swc-linux-x64-musl": "15.3.4", - "@next/swc-win32-arm64-msvc": "15.3.4", - "@next/swc-win32-x64-msvc": "15.3.4", + "@next/swc-darwin-arm64": "15.3.5", + "@next/swc-darwin-x64": "15.3.5", + "@next/swc-linux-arm64-gnu": "15.3.5", + "@next/swc-linux-arm64-musl": "15.3.5", + "@next/swc-linux-x64-gnu": "15.3.5", + "@next/swc-linux-x64-musl": "15.3.5", + "@next/swc-win32-arm64-msvc": "15.3.5", + "@next/swc-win32-x64-msvc": "15.3.5", "sharp": "^0.34.1" }, "peerDependencies": { diff --git a/package.json b/package.json index 20f0aba..a5ba79d 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@distube/ytdl-core": "^4.16.12", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@fontsource/geist-mono": "^5.2.7", + "@fontsource/geist-sans": "^5.2.5", "@google/genai": "^0.13.0", "@hcaptcha/react-hcaptcha": "^1.12.0", "@headlessui/react": "^2.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f39c55..3139a76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: '@dnd-kit/sortable': specifier: ^10.0.0 version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@fontsource/geist-mono': + specifier: ^5.2.7 + version: 5.2.7 + '@fontsource/geist-sans': + specifier: ^5.2.5 + version: 5.2.5 '@google/genai': specifier: ^0.13.0 version: 0.13.0 @@ -649,6 +655,12 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@fontsource/geist-mono@5.2.7': + resolution: {integrity: sha512-xVPVFISJg/K0VVd+aQN0Y7X/sw9hUcJPyDWFJ5GpyU3bHELhoRsJkPSRSHXW32mOi0xZCUQDOaPj1sqIFJ1FGg==} + + '@fontsource/geist-sans@5.2.5': + resolution: {integrity: sha512-anllOHyJbElRs9fV15TeDRqAeb1IKm4bSknPl6ZMoyPTx1BBy7logudcUwpNjmQLkzn4Q0JGQLRCUKJYoyST6A==} + '@google/genai@0.13.0': resolution: {integrity: sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==} engines: {node: '>=18.0.0'} @@ -3585,6 +3597,7 @@ packages: next@15.3.6: resolution: {integrity: sha512-oI6D1zbbsh6JzzZFDCSHnnx6Qpvd1fSkVJu/5d8uluqnxzuoqtodVZjYvNovooznUq8udSAiKp7MbwlfZ8Gm6w==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -4854,6 +4867,10 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@fontsource/geist-mono@5.2.7': {} + + '@fontsource/geist-sans@5.2.5': {} + '@google/genai@0.13.0': dependencies: google-auth-library: 9.15.1 diff --git a/src/App.tsx b/src/App.tsx index 0705162..6f1843d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,11 +6,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import VercelAnalytics from "@/components/VercelAnalytics"; import { SpeedInsights } from "@vercel/speed-insights/react"; -import { AuthProvider } from "@/hooks/useAuth"; +import { AuthProvider } from "@/providers/AuthProvider"; import { HelmetProvider } from "react-helmet-async"; import ErrorBoundary from "@/components/ErrorBoundary"; import { IconLoader2 } from "@tabler/icons-react"; +// Lazy Pages const Index = lazy(() => import("@/pages/Index")); const ResourcesHub = lazy(() => import("@/pages/ResourcesHub")); const Contact = lazy(() => import("@/pages/Contact")); @@ -25,6 +26,9 @@ const PlayerRenderer = lazy(() => import("@/pages/PlayerRenderer")); const Renderbot = lazy(() => import("@/pages/Renderbot")); const Account = lazy(() => import("@/pages/Account")); const Admin = lazy(() => import("@/pages/Admin")); +const BlogEditor = lazy(() => import("@/components/admin/BlogEditor")); +const ProfileEditor = lazy(() => import("@/components/profile/ProfileEditor")); + const FAQ = lazy(() => import("@/pages/FAQ")); const TOS = lazy(() => import("@/pages/TOS")); const Privacy = lazy(() => import("@/pages/Privacy")); @@ -36,8 +40,8 @@ const NotFound = lazy(() => import("@/pages/NotFound")); const Showcase = lazy(() => import("@/pages/Showcase")); const Changelogs = lazy(() => import("@/pages/Changelogs")); const Profile = lazy(() => import("@/pages/Profile")); - -const queryClient = new QueryClient(); +const Blogs = lazy(() => import("@/pages/Blogs")); +const BlogView = lazy(() => import("@/pages/BlogView")); const LoadingFallback = ({ message = "Loading..." }: { message?: string }) => (
@@ -48,6 +52,17 @@ const LoadingFallback = ({ message = "Loading..." }: { message?: string }) => ( const App = () => { const [queryClient] = useState(() => new QueryClient()); + + // Global Theme Initialization + useState(() => { + const theme = localStorage.getItem('theme') as 'light' | 'dark' || + (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); + if (theme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }); return ( @@ -86,7 +101,22 @@ const App = () => { /> } /> } /> + + +
+ } /> } /> + + + + } /> + + + + } /> } /> } /> } /> @@ -101,6 +131,8 @@ const App = () => { } /> } /> } /> + } /> + } /> diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index ab4926c..1bff345 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -22,7 +22,7 @@ const Footer = () => { const handleCartClick = () => { if (cartClicked) return; - + const canvas = document.createElement('canvas'); canvas.style.position = 'fixed'; canvas.style.inset = '0'; @@ -31,30 +31,30 @@ const Footer = () => { canvas.style.zIndex = '999'; canvas.style.pointerEvents = 'none'; document.body.appendChild(canvas); - + const myConfetti = confetti.create(canvas, { resize: true, useWorker: true }); - + myConfetti({ particleCount: 150, spread: 100, origin: { y: 0.8 } }); - + setTimeout(() => { document.body.removeChild(canvas); }, 3000); - + toast('Made with ❤️ by Yamura!', { description: 'And a little help from the community.', position: "bottom-center", duration: 3000, }); - + setCartClicked(true); - + if (cartButtonRef.current) { cartButtonRef.current.style.transform = 'translateX(150%)'; } @@ -65,8 +65,8 @@ const Footer = () => {
-
@@ -74,45 +74,45 @@ const Footer = () => {
Renderdragon - +

The ultimate hub for creators. Find free resources for your next project, including music, sound effects, images, and more.

- +
- Discord - - Twitter - - YouTube - - {
- +

Legal

    @@ -136,7 +136,7 @@ const Footer = () => {
- +

Navigate

  • @@ -144,6 +144,12 @@ const Footer = () => { Home
  • +
  • + + Blogs + NEW + +
  • Resources Hub @@ -172,7 +178,7 @@ const Footer = () => {
- +

Tools

    @@ -221,17 +227,17 @@ const Footer = () => {
- +
FAQ - + Terms - + Privacy @@ -253,7 +259,7 @@ const Footer = () => { © {currentYear} RenderDragon. All rights reserved.

-
)} -
-
+
-
@@ -315,9 +230,9 @@ const Navbar = () => {
- +
- @@ -585,10 +498,10 @@ const Navbar = () => {
- + {scrolled && (
-
@@ -596,9 +509,9 @@ const Navbar = () => { )}
- ); diff --git a/src/components/admin/AdminBlogsManager.tsx b/src/components/admin/AdminBlogsManager.tsx new file mode 100644 index 0000000..569d4d5 --- /dev/null +++ b/src/components/admin/AdminBlogsManager.tsx @@ -0,0 +1,148 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { IconPlus, IconEdit, IconTrash, IconEye } from "@tabler/icons-react"; +import { supabase } from "@/integrations/supabase/client"; +import { toast } from "sonner"; +import { format } from "date-fns"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; +import { IconLoader2 } from "@tabler/icons-react"; + +interface BlogPost { + id: string; + title: string; + slug: string; + published: boolean; + created_at: string; + author_id: string; // could fetch profile name if needed +} + +export default function AdminBlogsManager() { + const [blogs, setBlogs] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchBlogs = async () => { + setLoading(true); + const { data, error } = await supabase + .from("blogs") + .select("*") + .order("created_at", { ascending: false }); + + if (error) { + console.error("Error fetching blogs:", error); + toast.error("Failed to fetch blogs"); + } else { + setBlogs(data || []); + } + setLoading(false); + }; + + useEffect(() => { + fetchBlogs(); + }, []); + + const handleDelete = async (id: string) => { + const { error } = await supabase.from("blogs").delete().eq("id", id); + if (error) { + toast.error("Failed to delete blog"); + console.error(error); + } else { + toast.success("Blog deleted"); + fetchBlogs(); + } + }; + + return ( +
+
+
+

Blog Posts

+

Manage your blog content.

+
+ +
+ + + + {loading ? ( +
+ ) : blogs.length === 0 ? ( +
No blog posts found.
+ ) : ( + + + + Title + Status + Date + Actions + + + + {blogs.map(blog => ( + + + + {blog.title} + +
{blog.slug}
+
+ + + {blog.published ? "Published" : "Draft"} + + + + {format(new Date(blog.created_at), "MMM d, yyyy")} + + +
+ + + + + + + + + + Delete Blog Post? + + Are you sure you want to delete "{blog.title}"? This cannot be undone. + + + + Cancel + handleDelete(blog.id)} className="bg-red-600 hover:bg-red-700">Delete + + + +
+
+
+ ))} +
+
+ )} +
+
+
+ ); +} diff --git a/src/components/admin/AdminResourcesManager.tsx b/src/components/admin/AdminResourcesManager.tsx index 067aee1..4001b9c 100644 --- a/src/components/admin/AdminResourcesManager.tsx +++ b/src/components/admin/AdminResourcesManager.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; @@ -32,7 +32,7 @@ const AdminResourcesManager = () => { const categories = ['all', 'music', 'sfx', 'images', 'animations', 'fonts', 'presets']; - const fetchResources = async (isNewSearch = false) => { + const fetchResources = useCallback(async (isNewSearch = false) => { try { setLoading(true); const from = isNewSearch ? 0 : page * RESOURCES_PER_PAGE; @@ -54,7 +54,7 @@ const AdminResourcesManager = () => { const { data, error, count } = await query; if (error) throw error; - + setResources(prev => isNewSearch ? data || [] : [...prev, ...(data || [])]); if (count !== null) { @@ -73,10 +73,11 @@ const AdminResourcesManager = () => { } finally { setLoading(false); } - }; + }, [searchTerm, selectedCategory, page]); useEffect(() => { fetchResources(true); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchTerm, selectedCategory]); const loadMore = () => { @@ -88,6 +89,7 @@ const AdminResourcesManager = () => { if (page > 0) { fetchResources(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [page]); @@ -99,7 +101,7 @@ const AdminResourcesManager = () => { .eq('id', resource.id); if (error) throw error; - + toast.success('Resource deleted successfully'); fetchResources(true); setDeleteDialog({ open: false, resource: null }); @@ -236,13 +238,13 @@ const AdminResourcesManager = () => { />
)} - + {resource.description && (

{resource.description}

)} - +
{resource.credit && (
diff --git a/src/components/admin/BlogEditor.tsx b/src/components/admin/BlogEditor.tsx new file mode 100644 index 0000000..7a462d6 --- /dev/null +++ b/src/components/admin/BlogEditor.tsx @@ -0,0 +1,171 @@ +import { useState, useEffect } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { toast } from "sonner"; +import { IconLoader2, IconDeviceFloppy, IconEye, IconArrowLeft } from "@tabler/icons-react"; +import ReactMarkdown from "react-markdown"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { useAuth } from "@/hooks/useAuth"; + +const slugify = (text: string) => { + return text + .toString() + .toLowerCase() + .trim() + .replace(/\s+/g, '-') + .replace(/[^\w\-]+/g, '') + .replace(/\-\-+/g, '-'); +}; + +export default function BlogEditor() { + const { id } = useParams(); + const navigate = useNavigate(); + const { user } = useAuth(); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + + const [title, setTitle] = useState(""); + const [slug, setSlug] = useState(""); + const [content, setContent] = useState(""); + const [published, setPublished] = useState(false); + const [preview, setPreview] = useState(false); + + useEffect(() => { + if (id) { + loadBlog(id); + } + }, [id]); + + // Auto-generate slug from title if creating new + useEffect(() => { + if (!id && title) { + setSlug(slugify(title)); + } + }, [title, id]); + + const loadBlog = async (blogId: string) => { + setLoading(true); + const { data, error } = await supabase + .from("blogs") + .select("*") + .eq("id", blogId) + .single(); + + if (error) { + toast.error("Failed to load blog"); + console.error(error); + navigate("/admin"); + } else if (data) { + setTitle(data.title); + setSlug(data.slug); + setContent(data.content || ""); + setPublished(data.published || false); + } + setLoading(false); + }; + + const handleSave = async () => { + if (!title || !slug || !user) { + toast.error("Title and slug are required"); + return; + } + setSaving(true); + + const payload = { + title, + slug, + content, + published, + author_id: user.id, + updated_at: new Date().toISOString(), + }; + + try { + if (id) { + // Update + const { error } = await supabase + .from("blogs") + .update(payload) + .eq("id", id); + if (error) throw error; + toast.success("Blog updated saved"); + } else { + // Create + const { error } = await supabase + .from("blogs") + .insert([payload]); + if (error) throw error; + toast.success("Blog created successfully"); + navigate("/admin"); // Redirect or clear form + } + } catch (e: any) { + console.error("Error saving blog:", e); + toast.error(`Error saving: ${e.message}`); + } finally { + setSaving(false); + } + }; + + if (loading) return
; + + return ( +
+
+
+ +

{id ? "Edit Blog" : "New Blog"}

+
+
+ + +
+
+ + {!preview ? ( +
+
+ + setTitle(e.target.value)} placeholder="Blog Post Title" /> +
+ +
+ + setSlug(e.target.value)} placeholder="blog-post-slug" /> +
+ +
+ + +
+ +
+ +