├── src ├── api │ ├── auth.js │ ├── index.js │ └── user.js ├── config │ └── env.js ├── hooks │ ├── index.js │ ├── useAuth.js │ └── useFetch.js ├── redux │ └── store.js ├── context │ ├── index.js │ └── AuthContext.js ├── tests │ └── setupTests.js ├── utils │ ├── index.js │ ├── formatters.js │ ├── helpers.js │ ├── validators.js │ └── buttonUtils.ts ├── vite-env.d.ts ├── assets │ ├── images │ │ ├── logo-dark.png │ │ ├── logo-white.png │ │ └── main-logo.svg │ └── gif-Loader │ │ ├── PlanLoader.gif │ │ └── startupLoader.gif ├── types │ ├── react-syntax-highlighter.d.ts │ └── index.ts ├── lib │ ├── supabase.ts │ ├── realtime.ts │ ├── theme.ts │ └── theme.css ├── main.tsx ├── components │ ├── UserProfile │ │ ├── AdditionalInfo.tsx │ │ ├── StatCard.tsx │ │ ├── ProfileHeader.tsx │ │ ├── RecentActivity.tsx │ │ ├── ContactInfo.tsx │ │ ├── Achievements.tsx │ │ └── Badges.tsx │ ├── FeatureCard.tsx │ ├── ThemeToggle.tsx │ ├── PostCard.tsx │ ├── BugReportCard.tsx │ ├── MeetingRoom.tsx │ ├── VideoCall.tsx │ ├── BlogPreview.tsx │ ├── Button.tsx │ ├── footer.tsx │ ├── BugReportEditor.tsx │ ├── BugReportPreview.tsx │ ├── Navbar.tsx │ └── BlogEditor.tsx ├── index.css ├── pages │ ├── BlogCreate.tsx │ ├── BugReportCreate.tsx │ ├── auth │ │ ├── ForgotPassword.tsx │ │ ├── SignUp.tsx │ │ ├── SignIn.tsx │ │ └── ResetPassword.tsx │ ├── KnowledgeBaseDetail.tsx │ ├── Profile.tsx │ ├── BlogDetail.tsx │ ├── Home.tsx │ ├── Discussions.tsx │ ├── KnowledgeBase.tsx │ ├── BugReports.tsx │ ├── DiscussionDetail.tsx │ ├── Blog.tsx │ ├── Notifications.tsx │ ├── BugReportDetail.tsx │ └── Meetings.tsx └── App.tsx ├── vite.png ├── .env.example ├── .dockerignore ├── .github └── FUNDING.yml ├── postcss.config.js ├── tsconfig.json ├── .prettierrc ├── nginx.conf ├── vite.config.ts ├── .gitignore ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── tailwind.config.js ├── Dockerfile ├── eslint.config.js ├── LICENSE ├── package.json ├── supabase └── migrations │ └── 20250128160210_amber_scene.sql └── README.md /src/api/auth.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/env.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/context/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tests/setupTests.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | // Utility exports 2 | -------------------------------------------------------------------------------- /src/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | // Hook for authentication 2 | -------------------------------------------------------------------------------- /src/hooks/useFetch.js: -------------------------------------------------------------------------------- 1 | // Hook for fetching data 2 | -------------------------------------------------------------------------------- /src/utils/formatters.js: -------------------------------------------------------------------------------- 1 | // String/Date formatters 2 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | // Common helper functions 2 | -------------------------------------------------------------------------------- /src/context/AuthContext.js: -------------------------------------------------------------------------------- 1 | // Auth state management 2 | -------------------------------------------------------------------------------- /src/utils/validators.js: -------------------------------------------------------------------------------- 1 | // Form validation functions 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usama7365/Devhub/HEAD/vite.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VITE_SUPABASE_ANON_KEY= "ypur_anon_key" 2 | VITE_SUPABASE_URL= "your_supabase_url" -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .git 4 | Dockerfile 5 | docker-compose.yml 6 | .vscode 7 | .idea 8 | -------------------------------------------------------------------------------- /src/assets/images/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usama7365/Devhub/HEAD/src/assets/images/logo-dark.png -------------------------------------------------------------------------------- /src/assets/images/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usama7365/Devhub/HEAD/src/assets/images/logo-white.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [usama7365] 4 | patreon: feline411 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/assets/gif-Loader/PlanLoader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usama7365/Devhub/HEAD/src/assets/gif-Loader/PlanLoader.gif -------------------------------------------------------------------------------- /src/assets/gif-Loader/startupLoader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usama7365/Devhub/HEAD/src/assets/gif-Loader/startupLoader.gif -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/buttonUtils.ts: -------------------------------------------------------------------------------- 1 | export function buttonUtils( 2 | ...inputs: (string | boolean | null | undefined)[] 3 | ): string { 4 | return inputs.filter(Boolean).join(' '); 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "bracketSpacing": true, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /src/types/react-syntax-highlighter.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-syntax-highlighter' { 2 | export const Prism: any; 3 | export const Light: any; 4 | } 5 | 6 | declare module 'react-syntax-highlighter/dist/esm/styles/prism' { 7 | export const vscDarkPlus: any; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | 3 | const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; 4 | const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY; 5 | 6 | export const supabase = createClient(supabaseUrl, supabaseKey); 7 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html; 8 | try_files $uri /index.html; 9 | } 10 | 11 | error_page 404 /index.html; 12 | } 13 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | optimizeDeps: { 8 | exclude: ['lucide-react'], 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App.tsx'; 4 | import './index.css'; 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | package-lock.json 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DevHub Community 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/UserProfile/AdditionalInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { User } from '../../types'; 3 | import { Calendar } from 'lucide-react'; 4 | 5 | const AdditionalInfo: React.FC<{ user: User }> = ({ user }) => ( 6 |
7 |
8 | 9 | Joined {new Date(user.created_at).toLocaleDateString()} 10 |
11 |
12 | ); 13 | 14 | export default AdditionalInfo; 15 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /src/components/FeatureCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface FeatureCardProps { 4 | icon: React.ReactNode; 5 | title: string; 6 | description: string; 7 | } 8 | 9 | export const FeatureCard: React.FC = React.memo( 10 | ({ icon, title, description }) => { 11 | return ( 12 |
13 |
14 | {icon} 15 |
16 |

{title}

17 |

{description}

18 |
19 | ); 20 | } 21 | ); 22 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | @apply antialiased text-gray-900; 8 | } 9 | } 10 | 11 | @layer components { 12 | .btn { 13 | @apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500; 14 | } 15 | 16 | .btn-secondary { 17 | @apply inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/UserProfile/StatCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function StatCard({ 4 | icon, 5 | label, 6 | value, 7 | }: { 8 | icon: React.ReactNode; 9 | label: string; 10 | value: number | string; 11 | }) { 12 | return ( 13 |
14 |
15 |
{icon}
16 |
17 |
{value}
18 |
{label}
19 |
20 |
21 |
22 | ); 23 | } 24 | 25 | export default StatCard; 26 | -------------------------------------------------------------------------------- /src/lib/realtime.ts: -------------------------------------------------------------------------------- 1 | import { RealtimeChannel } from '@supabase/supabase-js'; 2 | import { supabase } from './supabase'; 3 | 4 | interface RealtimeMessage { 5 | type: 'notification' | 'meeting_invite' | 'blog_comment'; 6 | payload: any; 7 | } 8 | 9 | let channel: RealtimeChannel | null = null; 10 | 11 | export function initializeRealtime( 12 | userId: string, 13 | onMessage: (message: RealtimeMessage) => void 14 | ) { 15 | if (channel) { 16 | channel.unsubscribe(); 17 | } 18 | 19 | channel = supabase 20 | .channel(`user:${userId}`) 21 | .on('broadcast', { event: 'message' }, ({ payload }) => { 22 | onMessage(payload as RealtimeMessage); 23 | }) 24 | .subscribe(); 25 | 26 | return () => { 27 | if (channel) { 28 | channel.unsubscribe(); 29 | channel = null; 30 | } 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/UserProfile/ProfileHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { User } from '../../types'; 3 | 4 | const ProfileHeader: React.FC<{ user: User }> = ({ user }) => ( 5 |
6 |
7 | {user.username} 12 |
13 |

14 | {user.username} 15 |

16 |

{user.bio}

17 |
18 |
19 |
20 | ); 21 | 22 | export default ProfileHeader; 23 | -------------------------------------------------------------------------------- /src/components/UserProfile/RecentActivity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Post } from '../../types'; 3 | import { PostCard } from '../PostCard'; 4 | 5 | const RecentActivity: React.FC<{ posts: Post[] }> = ({ posts }) => ( 6 |
7 |

8 | Recent Activity 9 |

10 |
11 | {posts.map((post) => ( 12 | 13 | ))} 14 | {posts.length === 0 && ( 15 |

16 | No activity yet 17 |

18 | )} 19 |
20 |
21 | ); 22 | 23 | export default RecentActivity; 24 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | darkMode: 'class', 5 | theme: { 6 | extend: { 7 | colors: { 8 | // Nord theme colors 9 | nord: { 10 | 0: '#2E3440', 11 | 1: '#3B4252', 12 | 2: '#434C5E', 13 | 3: '#4C566A', 14 | snow0: '#D8DEE9', 15 | snow1: '#E5E9F0', 16 | snow2: '#ECEFF4', 17 | frost0: '#8FBCBB', 18 | frost1: '#88C0D0', 19 | frost2: '#81A1C1', 20 | frost3: '#5E81AC', 21 | aurora0: '#BF616A', 22 | aurora1: '#D08770', 23 | aurora2: '#EBCB8B', 24 | aurora3: '#A3BE8C', 25 | aurora4: '#B48EAD', 26 | }, 27 | }, 28 | }, 29 | }, 30 | plugins: [], 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/UserProfile/ContactInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { User, Post } from '../../types'; 4 | import { Github, Mail } from 'lucide-react'; 5 | 6 | const ContactInfo: React.FC<{ user: User }> = ({ user }) => ( 7 |
8 |
9 | 10 | {user.email} 11 |
12 | {user.github_username && ( 13 |
14 | 15 | 21 | {user.github_username} 22 | 23 |
24 | )} 25 |
26 | ); 27 | 28 | export default ContactInfo; 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js runtime as a parent image 2 | FROM node:20-alpine AS build 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json to install dependencies 8 | COPY package.json package-lock.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the rest of the application files 14 | COPY . . 15 | 16 | # Copy the .env file into the container 17 | COPY .env .env 18 | 19 | # Build the application 20 | RUN npm run build 21 | 22 | # Use a lightweight web server for serving static files 23 | FROM nginx:1.25.3-alpine 24 | 25 | # Set the working directory inside the container 26 | WORKDIR /usr/share/nginx/html 27 | 28 | # Remove the default nginx static assets 29 | RUN rm -rf ./* 30 | 31 | # Copy the built files from the previous stage 32 | COPY --from=build /app/dist . 33 | 34 | # Copy custom Nginx configuration file 35 | COPY nginx.conf /etc/nginx/conf.d/default.conf 36 | 37 | # Expose the port Nginx is running on 38 | EXPOSE 80 39 | 40 | # Start Nginx 41 | CMD ["nginx", "-g", "daemon off;"] 42 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | import react from 'eslint-plugin-react'; 4 | import reactHooks from 'eslint-plugin-react-hooks'; 5 | import prettier from 'eslint-plugin-prettier'; 6 | 7 | export default [ 8 | { 9 | ignores: ['dist', 'node_modules'], 10 | }, 11 | 12 | { 13 | files: ['**/*.{ts,tsx}'], 14 | languageOptions: { 15 | ecmaVersion: 'latest', 16 | sourceType: 'module', 17 | parser: tseslint.parser, 18 | parserOptions: { 19 | project: './tsconfig.app.json', 20 | tsconfigRootDir: process.cwd(), 21 | }, 22 | }, 23 | plugins: { 24 | '@typescript-eslint': tseslint.plugin, 25 | react: react, 26 | 'react-hooks': reactHooks, 27 | prettier: prettier, 28 | }, 29 | rules: { 30 | ...tseslint.configs.recommended.rules, 31 | ...react.configs.recommended.rules, 32 | ...reactHooks.configs.recommended.rules, 33 | 'prettier/prettier': 'error', 34 | }, 35 | settings: { 36 | react: { 37 | version: 'detect', 38 | }, 39 | }, 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Usama Aamir {Feline Predator} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/theme.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { persist } from 'zustand/middleware'; 3 | 4 | export type Theme = 'light' | 'dark' | 'sepia' | 'nord' | 'dracula' | 'system'; 5 | 6 | interface ThemeState { 7 | theme: Theme; 8 | setTheme: (theme: Theme) => void; 9 | } 10 | 11 | export const useTheme = create()( 12 | persist( 13 | (set) => ({ 14 | theme: 'system', 15 | setTheme: (theme) => { 16 | set({ theme }); 17 | const root = document.documentElement; 18 | const themes = [ 19 | 'theme-light', 20 | 'theme-dark', 21 | 'theme-sepia', 22 | 'theme-nord', 23 | 'theme-dracula', 24 | ]; 25 | 26 | // Remove all previous theme classes 27 | themes.forEach((t) => root.classList.remove(t)); 28 | 29 | // Apply new theme 30 | if (theme === 'system') { 31 | const isDark = window.matchMedia( 32 | '(prefers-color-scheme: dark)' 33 | ).matches; 34 | root.classList.add(isDark ? 'theme-dark' : 'theme-light'); 35 | } else { 36 | root.classList.add(`theme-${theme}`); 37 | } 38 | }, 39 | }), 40 | { 41 | name: 'theme-storage', // Local storage key 42 | } 43 | ) 44 | ); 45 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | username: string; 4 | email: string; 5 | avatar_url?: string; 6 | bio?: string; 7 | github_username?: string; 8 | created_at: string; 9 | } 10 | 11 | export interface Post { 12 | id: string; 13 | title: string; 14 | content: string; 15 | user_id: string; 16 | category: string; 17 | tags: string[]; 18 | upvotes: number; 19 | is_resolved: boolean; 20 | created_at: string; 21 | updated_at: string; 22 | } 23 | 24 | export interface Comment { 25 | id: string; 26 | content: string; 27 | user: User; 28 | post_id: string; 29 | is_accepted: boolean; 30 | upvotes: number; 31 | created_at: string; 32 | } 33 | 34 | export interface Article { 35 | id: string; 36 | title: string; 37 | content: string; 38 | description: string; 39 | user_id: string; 40 | category: string; 41 | tags: string[]; 42 | views: number; 43 | created_at: string; 44 | updated_at: string; 45 | } 46 | 47 | export interface Author { 48 | username: string; 49 | avatar_url: string; 50 | } 51 | 52 | export interface BlogPost { 53 | id: string; 54 | title: string; 55 | content: string; 56 | cover_image: string; 57 | created_at: string; 58 | tags: string[]; 59 | views: number; 60 | likes: number; 61 | author: Author; 62 | } 63 | 64 | export interface Meeting { 65 | id: string; 66 | title: string; 67 | description: string; 68 | start_time: string; 69 | duration: string; 70 | max_participants: number; 71 | room_id: string; 72 | host: User; 73 | } 74 | -------------------------------------------------------------------------------- /src/components/UserProfile/Achievements.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const dummyAchievements = [ 4 | { 5 | id: '1', 6 | name: 'Bug Hunter', 7 | description: 'Fixed 10 bugs', 8 | category: 'bugs', 9 | icon: '🔍', 10 | earned_at: '2024-02-01T00:00:00Z', 11 | }, 12 | { 13 | id: '2', 14 | name: 'Prolific Writer', 15 | description: 'Published 10 blog posts', 16 | category: 'blogs', 17 | icon: '✍️', 18 | earned_at: '2024-02-15T00:00:00Z', 19 | }, 20 | ]; 21 | 22 | const Achievements: React.FC<{ achievements: typeof dummyAchievements }> = ({ 23 | achievements, 24 | }) => ( 25 |
26 |

27 | Achievements 28 |

29 |
30 | {achievements.map((achievement) => ( 31 |
35 | {achievement.icon} 36 |
37 |

38 | {achievement.name} 39 |

40 |

41 | {achievement.description} 42 |

43 |
44 |
45 | ))} 46 |
47 |
48 | ); 49 | 50 | export default Achievements; 51 | -------------------------------------------------------------------------------- /src/pages/BlogCreate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { BlogEditor } from '../components/BlogEditor'; 4 | import { supabase } from '../lib/supabase'; 5 | 6 | export function BlogCreate() { 7 | const navigate = useNavigate(); 8 | 9 | const handleSave = async (content: { 10 | title: string; 11 | content: string; 12 | tags: string[]; 13 | }) => { 14 | try { 15 | const { 16 | data: { user }, 17 | } = await supabase.auth.getUser(); 18 | 19 | if (!user) { 20 | navigate('/signin'); 21 | return; 22 | } 23 | 24 | const { error } = await supabase.from('blog_posts').insert([ 25 | { 26 | title: content.title, 27 | content: content.content, 28 | tags: content.tags, 29 | user_id: user.id, 30 | }, 31 | ]); 32 | 33 | if (error) throw error; 34 | 35 | navigate('/blog'); 36 | } catch (error) { 37 | console.error('Error creating blog post:', error); 38 | // Handle error (show notification, etc.) 39 | } 40 | }; 41 | 42 | return ( 43 |
44 |
45 |
46 |

47 | Create Blog Post 48 |

49 |

50 | Share your knowledge and experiences with the community 51 |

52 |
53 | 54 | 55 |
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev-community-platform", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,json,md}\"", 10 | "lint": "eslint .", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@monaco-editor/react": "^4.6.0", 15 | "@supabase/supabase-js": "^2.39.7", 16 | "clsx": "^2.1.0", 17 | "date-fns": "^3.3.1", 18 | "framer-motion": "^12.4.7", 19 | "lucide-react": "^0.344.0", 20 | "react": "^18.3.1", 21 | "react-dom": "^18.3.1", 22 | "react-markdown": "^9.0.3", 23 | "react-quill": "^2.0.0", 24 | "react-router-dom": "^6.22.2", 25 | "react-syntax-highlighter": "^15.6.1", 26 | "rehype-raw": "^7.0.0", 27 | "remark-gfm": "^4.0.1", 28 | "sanitize-html": "^2.14.0", 29 | "tailwind-merge": "^2.2.1", 30 | "turndown": "^7.2.0", 31 | "zustand": "^5.0.3" 32 | }, 33 | "devDependencies": { 34 | "@eslint/js": "^9.19.0", 35 | "@types/react": "^18.3.5", 36 | "@types/react-dom": "^18.3.0", 37 | "@types/webrtc": "^0.0.44", 38 | "@typescript-eslint/eslint-plugin": "^8.22.0", 39 | "@typescript-eslint/parser": "^8.22.0", 40 | "@vitejs/plugin-react": "^4.3.1", 41 | "autoprefixer": "^10.4.18", 42 | "eslint": "^9.19.0", 43 | "eslint-config-prettier": "^10.0.1", 44 | "eslint-plugin-prettier": "^5.2.3", 45 | "eslint-plugin-react": "^7.37.4", 46 | "eslint-plugin-react-hooks": "^5.1.0", 47 | "eslint-plugin-react-refresh": "^0.4.18", 48 | "globals": "^15.9.0", 49 | "postcss": "^8.4.35", 50 | "prettier": "3.5.1", 51 | "tailwindcss": "^3.4.1", 52 | "typescript": "^5.7.3", 53 | "typescript-eslint": "^8.22.0", 54 | "vite": "^5.4.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/pages/BugReportCreate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { BugReportEditor } from '../components/BugReportEditor'; 4 | import { supabase } from '../lib/supabase'; 5 | 6 | export function BugReportCreate() { 7 | const navigate = useNavigate(); 8 | 9 | const handleSave = async (content: { 10 | title: string; 11 | content: string; 12 | tags: string[]; 13 | coverImage?: string; 14 | }) => { 15 | try { 16 | const { 17 | data: { user }, 18 | } = await supabase.auth.getUser(); 19 | 20 | if (!user) { 21 | navigate('/signin'); 22 | return; 23 | } 24 | 25 | const { error } = await supabase.from('bug_reports').insert([ 26 | { 27 | title: content.title, 28 | content: content.content, 29 | tags: content.tags, 30 | user_id: user.id || '', 31 | is_resolved: false, 32 | upvotes: 0, 33 | }, 34 | ]); 35 | 36 | if (error) throw error; 37 | 38 | navigate('/bug-reports'); 39 | } catch (error) { 40 | console.error('Error creating blog post:', error); 41 | // Handle error (show notification, etc.) 42 | } 43 | }; 44 | 45 | return ( 46 |
47 |
48 |
49 |

50 | Create Bug Report 51 |

52 |

53 | Document and troubleshoot technical issues with the community 54 |

55 |
56 | 57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/UserProfile/Badges.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const dummyBadges = [ 4 | { 5 | id: '1', 6 | name: 'Bug Fixer Silver', 7 | description: 'Fixed 25 bugs', 8 | category: 'bugs', 9 | level: 'silver', 10 | icon: '🥈', 11 | current_count: 27, 12 | requirement_count: 30, 13 | earned_at: '2024-02-10T00:00:00Z', 14 | }, 15 | { 16 | id: '2', 17 | name: 'Content Creator Gold', 18 | description: 'Created 50 pieces of content', 19 | category: 'content', 20 | level: 'gold', 21 | icon: '🥇', 22 | current_count: 52, 23 | requirement_count: 60, 24 | earned_at: '2024-02-20T00:00:00Z', 25 | }, 26 | ]; 27 | 28 | const Badges: React.FC<{ badges: typeof dummyBadges }> = ({ badges }) => ( 29 |
30 |

31 | Badges 32 |

33 |
34 | {badges.map((badge) => ( 35 |
39 |
40 | {badge.icon} 41 |

42 | {badge.name} 43 |

44 |
45 |

{badge.description}

46 |
47 |
53 |
54 |
55 | ))} 56 |
57 |
58 | ); 59 | 60 | export default Badges; 61 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { Sun, Moon, Coffee, Snowflake, Droplet } from 'lucide-react'; 3 | import { useTheme, type Theme } from '../lib/theme'; 4 | 5 | const themes: { value: Theme; label: string; icon: React.ReactNode }[] = [ 6 | { value: 'light', label: 'Light', icon: }, 7 | { value: 'dark', label: 'Dark', icon: }, 8 | { value: 'sepia', label: 'Sepia', icon: }, 9 | { value: 'nord', label: 'Nord', icon: }, 10 | { value: 'dracula', label: 'Dracula', icon: }, 11 | { value: 'system', label: 'System', icon: }, 12 | ]; 13 | 14 | export function ThemeToggle() { 15 | const { theme, setTheme } = useTheme(); 16 | const [isOpen, setIsOpen] = useState(false); 17 | 18 | const handleToggleMenu = useCallback(() => { 19 | setIsOpen((prev) => !prev); 20 | }, []); 21 | 22 | const handleMouseLeave = useCallback(() => { 23 | setIsOpen(false); 24 | }, []); 25 | 26 | const handleThemeChange = useCallback( 27 | (newTheme: Theme) => { 28 | setTheme(newTheme); 29 | setTimeout(() => setIsOpen(false), 100); 30 | }, 31 | [setTheme] 32 | ); 33 | 34 | const currentThemeIcon = themes.find((t) => t.value === theme)?.icon; 35 | 36 | return ( 37 |
38 | {/* Toggle Button */} 39 | 47 | 48 | {/* Dropdown Menu */} 49 | {isOpen && ( 50 |
54 | {themes.map((t) => ( 55 | 67 | ))} 68 |
69 | )} 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/PostCard.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { ThumbsUp, MessageSquare, Check } from 'lucide-react'; 3 | import { formatDistanceToNow } from 'date-fns'; 4 | import type { Post } from '../types'; 5 | 6 | interface PostCardProps { 7 | post: Post; 8 | } 9 | 10 | const renderTag = (tag: string) => ( 11 | 15 | {tag} 16 | 17 | ); 18 | 19 | const ResolvedBadge = () => ( 20 | 21 | 22 | Resolved 23 | 24 | ); 25 | 26 | const UpvoteButton = ({ count }: { count: number }) => ( 27 | 31 | ); 32 | 33 | const DiscussButton = ({ postId }: { postId: string }) => ( 34 | 38 | 39 | Discuss 40 | 41 | ); 42 | 43 | const TimeStamp = ({ date }: { date: string }) => ( 44 | 45 | {formatDistanceToNow(new Date(date), { addSuffix: true })} 46 | 47 | ); 48 | 49 | export function PostCard({ post }: PostCardProps) { 50 | return ( 51 |
52 |
53 |
54 | 58 | {post.title} 59 | 60 |
61 | {post.tags.map(renderTag)} 62 |
63 |
64 | {post.is_resolved && } 65 |
66 |

67 | {post.content} 68 |

69 |
70 |
71 | 72 | 73 |
74 | 75 |
76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/components/BugReportCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { ThumbsUp, MessageSquare, Check } from 'lucide-react'; 4 | import { formatDistanceToNow } from 'date-fns'; 5 | import type { Post } from '../types'; 6 | 7 | interface BugReportCardProps { 8 | post: Post; 9 | } 10 | 11 | const Tag: React.FC<{ tag: string }> = ({ tag }) => ( 12 | 13 | {tag} 14 | 15 | ); 16 | 17 | const ResolvedBadge: React.FC = () => ( 18 | 19 | 20 | Resolved 21 | 22 | ); 23 | 24 | const UpvoteButton: React.FC<{ count: number }> = ({ count }) => ( 25 | 29 | ); 30 | 31 | const DiscussButton: React.FC<{ postId: string }> = ({ postId }) => ( 32 | 36 | 37 | Discuss 38 | 39 | ); 40 | 41 | const TimeStamp: React.FC<{ date: string }> = ({ date }) => ( 42 | 43 | {formatDistanceToNow(new Date(date), { addSuffix: true })} 44 | 45 | ); 46 | 47 | export function BugReportCard({ post }: BugReportCardProps) { 48 | return ( 49 |
50 | {/* Title and Tags */} 51 |
52 |
53 | 57 | {post.title} 58 | 59 |
60 | {post.tags.map((tag) => ( 61 | 62 | ))} 63 |
64 |
65 | {post.is_resolved && } 66 |
67 | 68 | {/* Content */} 69 |

70 | {post.content} 71 |

72 | 73 | {/* Metadata */} 74 |
75 |
76 | 77 | 78 |
79 | 80 |
81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /supabase/migrations/20250128160210_amber_scene.sql: -------------------------------------------------------------------------------- 1 | /* 2 | # Initial Schema Setup for DevHub Platform 3 | 4 | 1. New Tables 5 | - `users` 6 | - Core user information and profile data 7 | - `posts` 8 | - Discussion posts and bug reports 9 | - `comments` 10 | - Responses to posts 11 | - `tags` 12 | - Categorization system for posts 13 | - `post_tags` 14 | - Many-to-many relationship between posts and tags 15 | 16 | 2. Security 17 | - Enable RLS on all tables 18 | - Add policies for authenticated users 19 | */ 20 | 21 | -- Users table 22 | CREATE TABLE IF NOT EXISTS users ( 23 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), 24 | username text UNIQUE NOT NULL, 25 | email text UNIQUE NOT NULL, 26 | avatar_url text, 27 | bio text, 28 | github_username text, 29 | created_at timestamptz DEFAULT now(), 30 | updated_at timestamptz DEFAULT now() 31 | ); 32 | 33 | -- Posts table 34 | CREATE TABLE IF NOT EXISTS posts ( 35 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), 36 | title text NOT NULL, 37 | content text NOT NULL, 38 | user_id uuid REFERENCES users(id) NOT NULL, 39 | category text NOT NULL, 40 | upvotes integer DEFAULT 0, 41 | is_resolved boolean DEFAULT false, 42 | created_at timestamptz DEFAULT now(), 43 | updated_at timestamptz DEFAULT now() 44 | ); 45 | 46 | -- Comments table 47 | CREATE TABLE IF NOT EXISTS comments ( 48 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), 49 | content text NOT NULL, 50 | user_id uuid REFERENCES users(id) NOT NULL, 51 | post_id uuid REFERENCES posts(id) NOT NULL, 52 | is_accepted boolean DEFAULT false, 53 | upvotes integer DEFAULT 0, 54 | created_at timestamptz DEFAULT now(), 55 | updated_at timestamptz DEFAULT now() 56 | ); 57 | 58 | -- Tags table 59 | CREATE TABLE IF NOT EXISTS tags ( 60 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), 61 | name text UNIQUE NOT NULL, 62 | description text, 63 | created_at timestamptz DEFAULT now() 64 | ); 65 | 66 | -- Post Tags junction table 67 | CREATE TABLE IF NOT EXISTS post_tags ( 68 | post_id uuid REFERENCES posts(id) ON DELETE CASCADE, 69 | tag_id uuid REFERENCES tags(id) ON DELETE CASCADE, 70 | PRIMARY KEY (post_id, tag_id) 71 | ); 72 | 73 | -- Enable Row Level Security 74 | ALTER TABLE users ENABLE ROW LEVEL SECURITY; 75 | ALTER TABLE posts ENABLE ROW LEVEL SECURITY; 76 | ALTER TABLE comments ENABLE ROW LEVEL SECURITY; 77 | ALTER TABLE tags ENABLE ROW LEVEL SECURITY; 78 | ALTER TABLE post_tags ENABLE ROW LEVEL SECURITY; 79 | 80 | -- Create policies 81 | CREATE POLICY "Users can read all users" 82 | ON users FOR SELECT 83 | TO authenticated 84 | USING (true); 85 | 86 | CREATE POLICY "Users can update own profile" 87 | ON users FOR UPDATE 88 | TO authenticated 89 | USING (auth.uid() = id); 90 | 91 | CREATE POLICY "Anyone can read posts" 92 | ON posts FOR SELECT 93 | TO authenticated 94 | USING (true); 95 | 96 | CREATE POLICY "Authenticated users can create posts" 97 | ON posts FOR INSERT 98 | TO authenticated 99 | WITH CHECK (auth.uid() = user_id); 100 | 101 | CREATE POLICY "Users can update own posts" 102 | ON posts FOR UPDATE 103 | TO authenticated 104 | USING (auth.uid() = user_id); 105 | 106 | CREATE POLICY "Anyone can read comments" 107 | ON comments FOR SELECT 108 | TO authenticated 109 | USING (true); 110 | 111 | CREATE POLICY "Authenticated users can create comments" 112 | ON comments FOR INSERT 113 | TO authenticated 114 | WITH CHECK (auth.uid() = user_id); 115 | 116 | CREATE POLICY "Users can update own comments" 117 | ON comments FOR UPDATE 118 | TO authenticated 119 | USING (auth.uid() = user_id); 120 | 121 | CREATE POLICY "Anyone can read tags" 122 | ON tags FOR SELECT 123 | TO authenticated 124 | USING (true); 125 | 126 | CREATE POLICY "Anyone can read post_tags" 127 | ON post_tags FOR SELECT 128 | TO authenticated 129 | USING (true); -------------------------------------------------------------------------------- /src/pages/auth/ForgotPassword.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Mail } from 'lucide-react'; 4 | import { supabase } from '../../lib/supabase'; 5 | import LOGO from '../../assets/images/main-logo.svg'; 6 | import { Button } from '../../components/Button'; 7 | export function ForgotPassword() { 8 | const [email, setEmail] = useState(''); 9 | const [loading, setLoading] = useState(false); 10 | const [error, setError] = useState(null); 11 | const [message, setMessage] = useState(null); 12 | 13 | const handleResetPassword = async (e: React.FormEvent) => { 14 | e.preventDefault(); 15 | setLoading(true); 16 | setError(null); 17 | setMessage(null); 18 | 19 | try { 20 | const { error } = await supabase.auth.resetPasswordForEmail(email); 21 | 22 | if (error) throw error; 23 | 24 | setMessage('Password reset link sent to your email.'); 25 | } catch (err) { 26 | setError(err instanceof Error ? err.message : 'An error occurred'); 27 | } finally { 28 | setLoading(false); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 |
35 |
36 | DevHub Logo 42 |

43 | DEV 44 | HUB 45 |

46 |
47 | 48 |
49 |

50 | Forgot Password 51 |

52 |
53 | 54 | {error && ( 55 |
56 | {error} 57 |
58 | )} 59 | 60 | {message && ( 61 |
62 | {message} 63 |
64 | )} 65 | 66 |
67 |
68 |

69 | Enter your email to receive a password reset link. 70 |

71 | setEmail(e.target.value)} 77 | className="w-full px-3 py-2 border border-[var(--border-color)] rounded bg-[var(--bg-primary)] text-[var(--text-primary)] focus:ring-[var(--accent)] focus:border-[var(--accent)]" 78 | placeholder="Email" 79 | /> 80 |
81 | 82 | 92 |
93 | 94 |
95 |

96 | Remember your password?{' '} 97 | 101 | Sign in 102 | 103 |

104 |
105 |
106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/assets/images/main-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/MeetingRoom.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { VideoCall } from './VideoCall'; 3 | import { Users, Video, Phone } from 'lucide-react'; 4 | 5 | interface Participant { 6 | id: string; 7 | name: string; 8 | avatar: string; 9 | } 10 | 11 | interface MeetingRoomProps { 12 | roomId: string; 13 | participants: Participant[]; 14 | onInvite: (email: string) => void; 15 | } 16 | 17 | export function MeetingRoom({ 18 | roomId, 19 | participants, 20 | onInvite, 21 | }: MeetingRoomProps) { 22 | const [isCallActive, setIsCallActive] = useState(false); 23 | const [inviteEmail, setInviteEmail] = useState(''); 24 | 25 | const handleInvite = useCallback( 26 | (e: React.FormEvent) => { 27 | e.preventDefault(); 28 | if (inviteEmail.trim()) { 29 | onInvite(inviteEmail.trim()); 30 | setInviteEmail(''); 31 | } 32 | }, 33 | [inviteEmail, onInvite] 34 | ); 35 | 36 | const handleEmailChange = useCallback( 37 | (e: React.ChangeEvent) => { 38 | setInviteEmail(e.target.value); 39 | }, 40 | [] 41 | ); 42 | 43 | const handleJoinCall = useCallback(() => { 44 | setIsCallActive(true); 45 | }, []); 46 | 47 | const handleCloseCall = useCallback(() => { 48 | setIsCallActive(false); 49 | }, []); 50 | 51 | const renderParticipant = useCallback( 52 | (participant: Participant) => ( 53 |
57 | {participant.name} 62 | {participant.name} 63 |
64 | ), 65 | [] 66 | ); 67 | 68 | return ( 69 |
70 | {isCallActive ? ( 71 | 72 | ) : ( 73 |
74 |
75 |

Meeting Room

76 | 83 |
84 | 85 |
86 |

87 | 88 | Participants ({participants.length}) 89 |

90 |
91 | {participants.map(renderParticipant)} 92 |
93 |
94 | 95 |
96 | 103 | 110 |
111 |
112 | )} 113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /src/lib/theme.css: -------------------------------------------------------------------------------- 1 | /* Light theme */ 2 | .theme-light { 3 | --bg-primary: #f9fafb; 4 | --bg-secondary: #f3f4f6; 5 | --text-primary: #111827; 6 | --text-secondary: #4b5563; 7 | --accent: #4f46e5; 8 | --border-color: #cdd6e6; 9 | --card-bg: #ffffff; 10 | --box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Soft shadow */ 11 | --text-hover: #4b5563; /* Light background hover effect */ 12 | --shadow-color: rgba(96, 90, 232, 0.25); 13 | } 14 | 15 | /* Dark theme */ 16 | .theme-dark { 17 | --bg-primary: #1f2937; 18 | --bg-secondary: #111827; 19 | --text-primary: #f9fafb; 20 | --text-secondary: #d1d5db; 21 | --accent: #6366f1; 22 | --border-color: #374151; 23 | --card-bg: #2d3748; 24 | --box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); /* Stronger shadow */ 25 | --shadow-color: rgba(56, 189, 248, 0.25); 26 | } 27 | 28 | /* Sepia theme */ 29 | .theme-sepia { 30 | --bg-primary: #fdf6e3; 31 | --bg-secondary: #eee8d5; 32 | --text-primary: #657b83; 33 | --text-secondary: #93a1a1; 34 | --accent: #b58900; 35 | --border-color: #d1c4a1; 36 | --card-bg: #f4e1a1; 37 | --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15); /* Medium shadow */ 38 | --shadow-color: rgba(180, 83, 9, 0.2); 39 | } 40 | 41 | /* Nord theme */ 42 | .theme-nord { 43 | --bg-primary: #2e3440; 44 | --bg-secondary: #3b4252; 45 | --text-primary: #eceff4; 46 | --text-secondary: #d8dee9; 47 | --accent: #88c0d0; 48 | --border-color: #4c566a; 49 | --card-bg: #3b4252; 50 | --box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); /* Deep shadow */ 51 | --shadow-color: rgba(136, 192, 208, 0.25); 52 | } 53 | 54 | /* Dracula theme */ 55 | .theme-dracula { 56 | --bg-primary: #282a36; 57 | --bg-secondary: #44475a; 58 | --text-primary: #f8f8f2; 59 | --text-secondary: #6272a4; 60 | --accent: #bd93f9; 61 | --border-color: #6272a4; 62 | --card-bg: #44475a; 63 | --box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4); /* Strong glow */ 64 | --shadow-color: rgba(189, 147, 249, 0.6); 65 | } 66 | 67 | /* Apply theme variables globally */ 68 | :root { 69 | background-color: var(--bg-primary); 70 | color: var(--text-primary); 71 | } 72 | 73 | body { 74 | background-color: var(--bg-secondary); 75 | } 76 | 77 | /* ReactQuill Toolbar Custom Styling */ 78 | .ql-toolbar { 79 | background: var(--bg-secondary) !important; /* Toolbar background */ 80 | border-color: var(--border-color) !important; 81 | } 82 | 83 | .ql-toolbar button { 84 | color: var(--text-secondary) !important; /* Default icon color */ 85 | } 86 | 87 | .ql-toolbar button:hover, 88 | .ql-toolbar button.ql-active { 89 | color: var(--text-primary) !important; /* Active & hover icon color */ 90 | } 91 | 92 | /* Dropdown styles */ 93 | .ql-picker { 94 | color: var(--text-secondary) !important; 95 | } 96 | 97 | .ql-picker-label { 98 | color: var(--text-secondary) !important; 99 | } 100 | 101 | .ql-picker-label:hover { 102 | color: var(--text-primary) !important; 103 | } 104 | 105 | /* Fix Quill Toolbar Icons */ 106 | .ql-toolbar button svg { 107 | fill: var(--text-secondary) !important; /* Default icon color */ 108 | stroke: var(--text-secondary) !important; /* Some icons use stroke */ 109 | } 110 | 111 | .ql-toolbar button:hover svg, 112 | .ql-toolbar button.ql-active svg { 113 | fill: var(--text-primary) !important; /* Active & hover icon color */ 114 | stroke: var(--text-primary) !important; 115 | } 116 | 117 | .ql-snow .ql-stroke { 118 | /* fill: var(--text-primary) !important; Active & hover icon color */ 119 | stroke: var(--text-primary) !important; 120 | } 121 | 122 | .ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options { 123 | background: var(--bg-secondary) !important; 124 | color: var(--text-primary) !important; 125 | border-color: var(--border-color) !important; 126 | } 127 | 128 | /* .ql-container.ql-snow { 129 | border: none !important; 130 | } */ 131 | 132 | .ql-toolbar.ql-snow + .ql-container.ql-snow { 133 | min-height: 350px !important; 134 | } 135 | 136 | /* Custom placeholder styling for ReactQuill */ 137 | .custom-quill .ql-editor.ql-blank::before { 138 | color: var(--text-secondary) !important; /* Use theme-based color */ 139 | font-style: italic; /* Optional: Make it visually distinct */ 140 | opacity: 0.8; /* Adjust opacity for better readability */ 141 | } 142 | -------------------------------------------------------------------------------- /src/components/VideoCall.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState, useCallback } from 'react'; 2 | import { Video, VideoOff, Mic, MicOff, PhoneOff } from 'lucide-react'; 3 | 4 | interface VideoCallProps { 5 | roomId: string; 6 | onClose: () => void; 7 | } 8 | 9 | export function VideoCall({ roomId, onClose }: VideoCallProps) { 10 | const [isVideoEnabled, setIsVideoEnabled] = useState(true); 11 | const [isAudioEnabled, setIsAudioEnabled] = useState(true); 12 | const localVideoRef = useRef(null); 13 | const remoteVideoRef = useRef(null); 14 | const localStreamRef = useRef(null); 15 | 16 | const initializeMediaStream = useCallback(async () => { 17 | try { 18 | if (!navigator.mediaDevices) { 19 | throw new Error('Media devices not supported'); 20 | } 21 | 22 | const stream = await navigator.mediaDevices.getUserMedia({ 23 | video: true, 24 | audio: true, 25 | }); 26 | 27 | localStreamRef.current = stream; 28 | 29 | if (localVideoRef.current) { 30 | localVideoRef.current.srcObject = stream; 31 | } 32 | } catch (err) { 33 | console.error('Error accessing media devices:', err); 34 | } 35 | }, []); 36 | 37 | const handleVideoToggle = useCallback(() => { 38 | setIsVideoEnabled((prev) => !prev); 39 | if (localStreamRef.current) { 40 | localStreamRef.current 41 | .getVideoTracks() 42 | .forEach((track) => (track.enabled = !isVideoEnabled)); 43 | } 44 | }, [isVideoEnabled]); 45 | 46 | const handleAudioToggle = useCallback(() => { 47 | setIsAudioEnabled((prev) => !prev); 48 | if (localStreamRef.current) { 49 | localStreamRef.current 50 | .getAudioTracks() 51 | .forEach((track) => (track.enabled = !isAudioEnabled)); 52 | } 53 | }, [isAudioEnabled]); 54 | 55 | const cleanupMediaStream = useCallback(() => { 56 | if (localStreamRef.current) { 57 | localStreamRef.current.getTracks().forEach((track) => track.stop()); 58 | localStreamRef.current = null; 59 | } 60 | }, []); 61 | 62 | useEffect(() => { 63 | initializeMediaStream(); 64 | return cleanupMediaStream; 65 | }, [roomId, initializeMediaStream, cleanupMediaStream]); 66 | 67 | return ( 68 |
69 |
70 |
71 |
85 | 86 |
87 | 101 | 115 | 121 |
122 |
123 |
124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /src/components/BlogPreview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ArrowLeft, Calendar } from 'lucide-react'; 3 | import { formatDistanceToNow } from 'date-fns'; 4 | import ReactMarkdown from 'react-markdown'; 5 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 6 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; 7 | import remarkGfm from 'remark-gfm'; 8 | import { dummyUsers } from '../lib/dummy-data'; 9 | 10 | interface BlogPreviewProps { 11 | blog: { 12 | title: string; 13 | content: string; 14 | tags: string[]; 15 | cover_image?: string; 16 | author?: { username: string; avatar_url: string }; 17 | created_at?: string; 18 | views?: number; 19 | likes?: number; 20 | }; 21 | onBack: () => void; 22 | } 23 | 24 | export function BlogPreview({ blog, onBack }: BlogPreviewProps) { 25 | const author = blog.author || dummyUsers[0]; 26 | 27 | return ( 28 |
29 | {/* Back Button */} 30 | 37 | 38 | {/* Cover Image */} 39 | {blog.cover_image && ( 40 | {blog.title} 45 | )} 46 | 47 | {/* Author & Date */} 48 |
49 | {author.username} 54 |
55 |

{author.username}

56 |
57 | 58 | {blog.created_at 59 | ? formatDistanceToNow(new Date(blog.created_at), { 60 | addSuffix: true, 61 | }) 62 | : 'Just now'} 63 |
64 |
65 |
66 | 67 | {/* Blog Title */} 68 |

{blog.title}

69 | 70 | {/* Tags */} 71 |
72 | {blog.tags.map((tag) => ( 73 | 77 | {tag} 78 | 79 | ))} 80 |
81 | 82 | {/* Blog Content (Markdown Rendering) */} 83 |
84 | 96 | {String(children).replace(/\n$/, '')} 97 | 98 | ) : ( 99 | 103 | {children} 104 | 105 | ); 106 | }, 107 | ul: ({ node, ...props }) => ( 108 |
    109 | ), 110 | ol: ({ node, ...props }) => ( 111 |
      112 | ), 113 | li: ({ node, ...props }) =>
    1. , 114 | img: ({ node, ...props }) => ( 115 | 116 | ), 117 | a: ({ href, children }) => ( 118 | 124 | {children} 125 | 126 | ), 127 | }} 128 | > 129 | {blog.content} 130 | 131 |
132 |
133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /src/pages/auth/SignUp.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { Github, Mail } from 'lucide-react'; 4 | import { supabase } from '../../lib/supabase'; 5 | import LOGO from '../../assets/images/main-logo.svg'; 6 | import { Button } from '../../components/Button'; 7 | export function SignUp() { 8 | const navigate = useNavigate(); 9 | const [formData, setFormData] = useState({ 10 | email: '', 11 | password: '', 12 | username: '', 13 | }); 14 | const [loading, setLoading] = useState(false); 15 | const [error, setError] = useState(null); 16 | 17 | const handleChange = (e: React.ChangeEvent) => { 18 | const { name, value } = e.target; 19 | setFormData((prev) => ({ ...prev, [name]: value })); 20 | }; 21 | 22 | const handleSignUp = async (e: React.FormEvent) => { 23 | e.preventDefault(); 24 | setLoading(true); 25 | setError(null); 26 | 27 | try { 28 | const { error: signUpError } = await supabase.auth.signUp({ 29 | email: formData.email, 30 | password: formData.password, 31 | options: { data: { username: formData.username } }, 32 | }); 33 | 34 | if (signUpError) throw signUpError; 35 | 36 | const { error: profileError } = await supabase 37 | .from('users') 38 | .insert([{ username: formData.username, email: formData.email }]); 39 | 40 | if (profileError) throw profileError; 41 | 42 | navigate('/'); 43 | } catch (err) { 44 | setError(err instanceof Error ? err.message : 'An error occurred'); 45 | } finally { 46 | setLoading(false); 47 | } 48 | }; 49 | 50 | const handleGithubSignUp = async () => { 51 | try { 52 | const { error } = await supabase.auth.signInWithOAuth({ 53 | provider: 'github', 54 | }); 55 | if (error) throw error; 56 | } catch (err) { 57 | setError(err instanceof Error ? err.message : 'An error occurred'); 58 | } 59 | }; 60 | 61 | return ( 62 |
63 |
64 | {/* Add Logo here */} 65 |
66 | DevHub Logo 72 |

73 | DEV 74 | HUB 75 |

76 |
77 | 78 |
79 |

80 | Create your account 81 |

82 |

83 | or{' '} 84 | 88 | sign in 89 | 90 |

91 |
92 | 93 | {error && ( 94 |
95 | {error} 96 |
97 | )} 98 | 99 |
100 |
101 | {['username', 'email', 'password'].map((field) => ( 102 | 112 | ))} 113 |
114 | 115 | 124 |
125 | 126 |
127 |

or continue with

128 | 136 |
137 |
138 |
139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /src/pages/KnowledgeBaseDetail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate, useParams } from 'react-router-dom'; 3 | import { Eye, ArrowLeft } from 'lucide-react'; 4 | import { formatDistanceToNow } from 'date-fns'; 5 | import { dummyArticles, dummyUsers } from '../lib/dummy-data'; 6 | import ReactMarkdown from 'react-markdown'; 7 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 8 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; 9 | 10 | // Types 11 | interface CodeProps { 12 | node: any; 13 | inline: boolean; 14 | className: string; 15 | children: React.ReactNode; 16 | [key: string]: any; 17 | } 18 | 19 | // Code Block component 20 | const CodeBlock: React.FC = ({ 21 | inline, 22 | className, 23 | children, 24 | ...props 25 | }) => { 26 | const match = /language-(\w+)/.exec(className || ''); 27 | 28 | if (!inline && match) { 29 | return ( 30 | 36 | {String(children).replace(/\n$/, '')} 37 | 38 | ); 39 | } 40 | 41 | return ( 42 | 43 | {children} 44 | 45 | ); 46 | }; 47 | 48 | const markdownComponents = { 49 | code: CodeBlock, 50 | }; 51 | 52 | export function KnowledgeBaseDetail() { 53 | const navigate = useNavigate(); 54 | const { id } = useParams<{ id: string }>(); 55 | 56 | const articleData = React.useMemo(() => { 57 | const article = dummyArticles.find((a) => a.id === id); 58 | const author = dummyUsers.find((u) => u.id === article?.user_id); 59 | return { article, author }; 60 | }, [id]); 61 | 62 | const handleBackClick = React.useCallback(() => { 63 | navigate('/knowledge-base'); 64 | }, [navigate]); 65 | 66 | if (!articleData.article || !articleData.author) { 67 | return ( 68 |
69 | Article not found 70 |
71 | ); 72 | } 73 | 74 | const { article, author } = articleData; 75 | 76 | return ( 77 |
78 |
79 | 86 |
87 |
88 |
89 | {/* Author Info and Views */} 90 |
91 |
92 | {author.username} 97 |
98 |

99 | {author.username} 100 |

101 |

102 | {formatDistanceToNow(new Date(article.created_at), { 103 | addSuffix: true, 104 | })} 105 |

106 |
107 |
108 |
109 | 110 | {article.views} views 111 |
112 |
113 | 114 | {/* Article Title */} 115 |

116 | {article.title} 117 |

118 | 119 | {/* Tags and Category */} 120 |
121 |
122 | {article.tags.map((tag) => ( 123 | 127 | {tag} 128 | 129 | ))} 130 |
131 | 132 | {article.category} 133 | 134 |
135 | 136 | {/* Article Content */} 137 |
138 | 139 | {article.content} 140 | 141 |
142 |
143 |
144 |
145 | ); 146 | } 147 | -------------------------------------------------------------------------------- /src/pages/auth/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { Github, Mail } from 'lucide-react'; 4 | import { supabase } from '../../lib/supabase'; 5 | import LOGO from '../../assets/images/main-logo.svg'; 6 | import { Button } from '../../components/Button'; 7 | export function SignIn() { 8 | const navigate = useNavigate(); 9 | const [formData, setFormData] = useState({ 10 | email: '', 11 | password: '', 12 | username: '', 13 | }); 14 | const [loading, setLoading] = useState(false); 15 | const [error, setError] = useState(null); 16 | 17 | const handleSignIn = async (e: React.FormEvent) => { 18 | e.preventDefault(); 19 | setLoading(true); 20 | setError(null); 21 | 22 | try { 23 | const { error: signUpError } = await supabase.auth.signUp({ 24 | email: formData.email, 25 | password: formData.password, 26 | options: { data: { username: formData.username } }, 27 | }); 28 | 29 | if (signUpError) throw signUpError; 30 | 31 | const { error: profileError } = await supabase 32 | .from('users') 33 | .insert([{ username: formData.username, email: formData.email }]); 34 | 35 | if (profileError) throw profileError; 36 | 37 | navigate('/'); 38 | } catch (err) { 39 | setError(err instanceof Error ? err.message : 'An error occurred'); 40 | } finally { 41 | setLoading(false); 42 | } 43 | }; 44 | 45 | const handleChange = (e: React.ChangeEvent) => { 46 | const { name, value } = e.target; 47 | setFormData((prev) => ({ ...prev, [name]: value })); 48 | }; 49 | 50 | const handleGithubSignIn = async () => { 51 | try { 52 | const { error } = await supabase.auth.signInWithOAuth({ 53 | provider: 'github', 54 | }); 55 | if (error) throw error; 56 | } catch (err) { 57 | setError(err instanceof Error ? err.message : 'An error occurred'); 58 | } 59 | }; 60 | 61 | return ( 62 |
63 |
64 |
65 | DevHub Logo 71 |

72 | DEV 73 | HUB 74 |

75 |
76 | 77 |
78 |

79 | Sign in to DevHub 80 |

81 |

82 | or{' '} 83 | 87 | create a new account 88 | 89 |

90 |
91 | 92 | {error && ( 93 |
94 | {error} 95 |
96 | )} 97 | 98 |
99 |
100 | {['email', 'password'].map((field) => ( 101 | 111 | ))} 112 |
113 | 114 | {/* Forgot Password Link */} 115 |
116 | 120 | Forgot Password? 121 | 122 |
123 | 132 |
133 | 134 |
135 |

or continue with

136 | 144 |
145 |
146 |
147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import type { LucideIcon } from 'lucide-react'; 4 | import { Loader2, ArrowRight } from 'lucide-react'; 5 | import { buttonUtils } from '../utils/buttonUtils'; 6 | 7 | type ButtonVariant = 8 | | 'solid' 9 | | 'soft' 10 | | 'outlined' 11 | | 'subtle' 12 | | 'brand' 13 | | 'social' 14 | | 'link'; 15 | 16 | export interface ButtonProps 17 | extends React.ButtonHTMLAttributes { 18 | variant?: ButtonVariant; // defines custom styling for buttons(hover, border etc) 19 | href?: string; 20 | isExternal?: boolean; 21 | isLoading?: boolean; 22 | fullWidth?: boolean; 23 | leftIcon?: LucideIcon; 24 | rightIcon?: LucideIcon; 25 | withArrow?: boolean; 26 | } 27 | 28 | // Styles configuration 29 | const styles = { 30 | base: buttonUtils( 31 | 'inline-flex items-center justify-center font-medium rounded-md transition-all duration-300', 32 | 'disabled:opacity-50 disabled:cursor-not-allowed group whitespace-nowrap', 33 | 'px-4 py-2 text-base' // Default Tailwind button sizing 34 | ), 35 | variant: { 36 | solid: 37 | 'bg-[var(--accent)] text-[var(--bg-primary)] hover:opacity-95 border border-transparent hover:scale-[1.02] hover:shadow-lg hover:shadow-[var(--shadow-color)]/20 active:scale-[0.98] transform transition-all duration-300', 38 | soft: 'bg-[var(--bg-secondary)] text-[var(--text-primary)] hover:bg-opacity-90 border border-transparent hover:shadow-md hover:shadow-[var(--shadow-color)]/10 active:scale-[0.98] transform transition-all duration-300', 39 | outlined: 40 | 'border border-[var(--accent)] text-[var(--accent)] hover:bg-[var(--accent)]/10 hover:border-[var(--accent)] hover:shadow-md hover:shadow-[var(--shadow-color)]/10 active:scale-[0.98] transform transition-all duration-300', 41 | subtle: 42 | 'text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]/80 border border-transparent hover:shadow-sm active:scale-[0.98] transform transition-all duration-300', 43 | brand: 44 | 'bg-[var(--accent)] text-[var(--bg-primary)] hover:bg-[var(--accent)]/90 border border-transparent hover:scale-[1.02] hover:shadow-lg hover:shadow-[var(--shadow-color)]/20 active:scale-[0.98] transform transition-all duration-300', 45 | social: 46 | 'border border-[var(--border-color)] text-[var(--text-primary)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-secondary)]/90 hover:border-[var(--text-secondary)] hover:shadow-md hover:shadow-[var(--shadow-color)]/10 active:scale-[0.98] transform transition-all duration-300', 47 | link: 'font-medium text-[var(--accent)] hover:text-[var(--accent)]/90 p-0 hover:underline decoration-2 underline-offset-4 transition-all duration-300', 48 | }, 49 | icon: { 50 | base: 'transition-transform duration-300 group-hover:scale-110 flex-shrink-0', 51 | spacing: { 52 | left: 'mr-2', 53 | right: 'ml-2', 54 | }, 55 | }, 56 | } as const; 57 | 58 | export const Button = React.forwardRef( 59 | ( 60 | { 61 | children, 62 | variant = 'solid', 63 | href, 64 | isExternal = false, 65 | isLoading = false, 66 | fullWidth = false, 67 | leftIcon: LeftIcon, 68 | rightIcon: RightIcon, 69 | withArrow = false, 70 | className = '', 71 | disabled, 72 | ...props 73 | }, 74 | ref 75 | ) => { 76 | const buttonClasses = buttonUtils( 77 | styles.base, 78 | styles.variant[variant] || styles.variant.solid, 79 | fullWidth && 'w-full', // Ensures full-width works dynamically 80 | className 81 | ); 82 | 83 | const iconClasses = (isLeft: boolean) => 84 | buttonUtils( 85 | styles.icon.base, 86 | isLeft ? styles.icon.spacing.left : styles.icon.spacing.right 87 | ); 88 | 89 | const content = ( 90 | <> 91 | {isLoading ? ( 92 | 98 | ) : ( 99 | LeftIcon && 100 | )} 101 | 102 | {children} 103 | {withArrow && ( 104 | 110 | )} 111 | 112 | {!isLoading && RightIcon && ( 113 | 114 | )} 115 | 116 | ); 117 | 118 | return href ? ( 119 | isExternal ? ( 120 | 126 | {content} 127 | 128 | ) : ( 129 | 130 | {content} 131 | 132 | ) 133 | ) : ( 134 | 142 | ); 143 | } 144 | ); 145 | 146 | Button.displayName = 'Button'; 147 | -------------------------------------------------------------------------------- /src/pages/Profile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { Award, Star, Trophy, Target, Zap, Heart } from 'lucide-react'; 4 | import { dummyUsers, dummyPosts } from '../lib/dummy-data'; 5 | import ProfileHeader from '../components/UserProfile/ProfileHeader'; 6 | import ContactInfo from '../components/UserProfile/ContactInfo'; 7 | import AdditionalInfo from '../components/UserProfile/AdditionalInfo'; 8 | import RecentActivity from '../components/UserProfile/RecentActivity'; 9 | import StatCard from '../components/UserProfile/StatCard'; 10 | import Achievements from '../components/UserProfile/Achievements'; 11 | import Badges from '../components/UserProfile/Badges'; 12 | 13 | const dummyAchievements = [ 14 | { 15 | id: '1', 16 | name: 'Bug Hunter', 17 | description: 'Fixed 10 bugs', 18 | category: 'bugs', 19 | icon: '🔍', 20 | earned_at: '2024-02-01T00:00:00Z', 21 | }, 22 | { 23 | id: '2', 24 | name: 'Prolific Writer', 25 | description: 'Published 10 blog posts', 26 | category: 'blogs', 27 | icon: '✍️', 28 | earned_at: '2024-02-15T00:00:00Z', 29 | }, 30 | ]; 31 | 32 | const dummyBadges = [ 33 | { 34 | id: '1', 35 | name: 'Bug Fixer Silver', 36 | description: 'Fixed 25 bugs', 37 | category: 'bugs', 38 | level: 'silver', 39 | icon: '🥈', 40 | current_count: 27, 41 | requirement_count: 30, 42 | earned_at: '2024-02-10T00:00:00Z', 43 | }, 44 | { 45 | id: '2', 46 | name: 'Content Creator Gold', 47 | description: 'Created 50 pieces of content', 48 | category: 'content', 49 | level: 'gold', 50 | icon: '🥇', 51 | current_count: 52, 52 | requirement_count: 60, 53 | earned_at: '2024-02-20T00:00:00Z', 54 | }, 55 | ]; 56 | 57 | const dummyStats = { 58 | bugs_resolved: 27, 59 | blogs_written: 15, 60 | discussions_started: 32, 61 | discussions_participated: 84, 62 | kb_articles_written: 8, 63 | total_upvotes_received: 156, 64 | contribution_streak: 7, 65 | level: 3, 66 | }; 67 | 68 | const useUserData = (username: string | undefined) => { 69 | return useMemo(() => { 70 | if (!username) return null; 71 | return dummyUsers.find((u) => u.username === username); 72 | }, [username]); 73 | }; 74 | 75 | const useUserPosts = (userId: string | undefined) => { 76 | return useMemo(() => { 77 | if (!userId) return []; 78 | return dummyPosts.filter((post) => post.user_id === userId); 79 | }, [userId]); 80 | }; 81 | 82 | export function Profile() { 83 | const { username } = useParams<{ username: string }>(); 84 | const user = useUserData(username); 85 | const userPosts = useUserPosts(user?.id); 86 | 87 | if (!user) { 88 | return
User not found
; 89 | } 90 | 91 | return ( 92 |
93 |
94 |
95 |
96 |
97 | 98 |
99 | 100 | 101 |
102 | {/* Contribution Stats */} 103 |

104 | Contributions 105 |

106 |
107 | } 109 | label="Content Creator" 110 | value={dummyStats.level} 111 | /> 112 | } 114 | label="Likes" 115 | value={dummyStats.total_upvotes_received} 116 | /> 117 | } 119 | label="Bugs Resolved" 120 | value={dummyStats.bugs_resolved} 121 | /> 122 | } 124 | label="Blog Posts" 125 | value={dummyStats.blogs_written} 126 | /> 127 | } 129 | label="Discussions" 130 | value={dummyStats.discussions_started} 131 | /> 132 | } 134 | label="Contribution Streak" 135 | value={`${dummyStats.contribution_streak} days`} 136 | /> 137 |
138 | {/* Achievements */} 139 |
140 | 141 |
{' '} 142 | {/* Badges */} 143 |
144 | 145 |
{' '} 146 | 147 |
148 |
149 |
150 |
151 | ); 152 | } 153 | export default Profile; 154 | -------------------------------------------------------------------------------- /src/pages/BlogDetail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate, useParams } from 'react-router-dom'; 3 | import { ArrowLeft, Calendar, Eye, Heart } from 'lucide-react'; 4 | import { formatDistanceToNow } from 'date-fns'; 5 | import { dummyBlogPosts } from '../lib/dummy-data'; 6 | import ReactMarkdown from 'react-markdown'; 7 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 8 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; 9 | 10 | export function BlogDetail() { 11 | const navigate = useNavigate(); 12 | 13 | const { id } = useParams<{ id: string }>(); 14 | const post = dummyBlogPosts.find((p) => p.id === id); 15 | 16 | if (!post) { 17 | return ( 18 |
19 | Blog post not found 20 |
21 | ); 22 | } 23 | 24 | return ( 25 |
26 |
27 | 34 |
35 |
36 | {/* Blog Post Section */} 37 |
38 | {/* Cover Image */} 39 | {post.title} 44 | 45 | {/* Blog Post Content */} 46 |
47 | {/* Author Info and Metadata */} 48 |
49 |
50 | {post.author.username} 55 |
56 |

57 | {post.author.username} 58 |

59 |
60 | 61 | {formatDistanceToNow(new Date(post.created_at), { 62 | addSuffix: true, 63 | })} 64 |
65 |
66 |
67 |
68 | 69 | 70 | {post.views} views 71 | 72 | 73 | 74 | {post.likes} likes 75 | 76 |
77 |
78 | 79 | {/* Title */} 80 |

81 | {post.title} 82 |

83 | 84 | {/* Tags */} 85 |
86 | {post.tags.map((tag) => ( 87 | 91 | {tag} 92 | 93 | ))} 94 |
95 | 96 | {/* Content */} 97 |
98 | 109 | {String(children).replace(/\n$/, '')} 110 | 111 | ) : ( 112 | 113 | {children} 114 | 115 | ); 116 | }, 117 | img({ node, ...props }) { 118 | return ( 119 | 120 | ); 121 | }, 122 | }} 123 | > 124 | {post.content} 125 | 126 |
127 |
128 |
129 |
130 |
131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | Routes, 5 | Route, 6 | useLocation, 7 | } from 'react-router-dom'; 8 | import { motion } from 'framer-motion'; 9 | import startupLoader from './assets/gif-Loader/startupLoader.gif'; 10 | import planLoader from './assets/gif-Loader/PlanLoader.gif'; 11 | import { Navbar } from './components/Navbar'; 12 | import { Home } from './pages/Home'; 13 | import { KnowledgeBase } from './pages/KnowledgeBase'; 14 | import { KnowledgeBaseDetail } from './pages/KnowledgeBaseDetail'; 15 | import { Discussions } from './pages/Discussions'; 16 | import { DiscussionDetail } from './pages/DiscussionDetail'; 17 | import { BugReports } from './pages/BugReports'; 18 | import { BugReportDetail } from './pages/BugReportDetail'; 19 | import { Blog } from './pages/Blog'; 20 | import { BlogDetail } from './pages/BlogDetail'; 21 | import { Meetings } from './pages/Meetings'; 22 | import { Profile } from './pages/Profile'; 23 | import { SignIn } from './pages/auth/SignIn'; 24 | import { SignUp } from './pages/auth/SignUp'; 25 | import { ForgotPassword } from './pages/auth/ForgotPassword'; 26 | import { ResetPassword } from './pages/auth/ResetPassword'; 27 | import { useTheme } from './lib/theme'; 28 | import './lib/theme.css'; 29 | import { Footer } from './components/footer'; 30 | import { BlogCreate } from './pages/BlogCreate'; 31 | import { MeetingSchedule } from './pages/MeetingSchedule'; 32 | import { CodeEditor } from './pages/CodeEditor'; 33 | import { Notifications } from './pages/Notifications'; 34 | import { BugReportCreate } from './pages/BugReportCreate'; 35 | 36 | import 'react-quill/dist/quill.snow.css'; 37 | 38 | function App() { 39 | return ( 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | function MainApp() { 47 | const [loading, setLoading] = useState(true); 48 | const { theme } = useTheme(); 49 | const location = useLocation(); 50 | 51 | useEffect(() => { 52 | if (location.pathname === '/') { 53 | const timer = setTimeout(() => { 54 | setLoading(false); 55 | }, 2000); 56 | return () => clearTimeout(timer); 57 | } else { 58 | setLoading(false); 59 | } 60 | }, [location.pathname]); 61 | 62 | useEffect(() => { 63 | const root = document.documentElement; 64 | const themes = [ 65 | 'theme-light', 66 | 'theme-dark', 67 | 'theme-sepia', 68 | 'theme-nord', 69 | 'theme-dracula', 70 | ]; 71 | themes.forEach((t) => root.classList.remove(t)); 72 | if (theme === 'system') { 73 | const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 74 | root.classList.add(isDark ? 'theme-dark' : 'theme-light'); 75 | } else { 76 | root.classList.add(`theme-${theme}`); 77 | } 78 | }, [theme]); 79 | 80 | if (loading && location.pathname === '/') { 81 | return ( 82 |
83 | Startup Loader 84 | 90 | WELCOME TO DEVHUB COMMUNITY 91 | 92 |
93 | ); 94 | } 95 | 96 | return ( 97 |
98 | 99 |
100 | 103 | Loading... 104 |
105 | } 106 | > 107 | 108 | } /> 109 | } /> 110 | } /> 111 | } /> 112 | } 115 | /> 116 | } /> 117 | } /> 118 | } /> 119 | } /> 120 | } /> 121 | } /> 122 | } /> 123 | } /> 124 | } /> 125 | } /> 126 | } /> 127 | } /> 128 | } /> 129 | } /> 130 | } /> 131 | 132 | 133 | 134 |
135 |
136 | ); 137 | } 138 | 139 | export default App; 140 | -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Github, Twitter, Linkedin, Mail, Heart } from 'lucide-react'; 4 | import LOGO from '../assets/images/main-logo.svg'; 5 | 6 | export function Footer() { 7 | const days = [ 8 | 'Sunday', 9 | 'Monday', 10 | 'Tuesday', 11 | 'Wednesday', 12 | 'Thursday', 13 | 'Friday', 14 | 'Saturday', 15 | ]; 16 | const today = new Date().getDay(); 17 | 18 | return ( 19 |
20 |
21 |
22 |
23 | 27 | DevHub Logo 28 |

29 | DEV 30 | HUB 31 |

32 | 33 | 34 |

35 | A community platform for developers to share knowledge, discuss 36 | ideas, and grow together. 37 |

38 | 58 |
59 | 60 |
61 |

62 | Resources 63 |

64 |
    65 |
  • 66 | 70 | Knowledge Base 71 | 72 |
  • 73 |
  • 74 | 78 | Blog 79 | 80 |
  • 81 |
  • 82 | 86 | Discussions 87 | 88 |
  • 89 |
  • 90 | 94 | Bug Reports 95 | 96 |
  • 97 |
98 |
99 | 100 |
101 |

102 | Contact 103 |

104 | 123 |
124 |
125 |
126 |
127 |

128 | Made with 129 | 130 | by DevHub Team 131 | || 132 |

133 | 134 |

135 | Have a nice {days[today]}! 136 |

137 |
138 |
139 |
140 |
141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /src/components/BugReportEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from 'react'; 2 | import { Save, Image, Link as LinkIcon, Eye, Edit } from 'lucide-react'; 3 | import ReactQuill from 'react-quill'; 4 | import 'react-quill/dist/quill.snow.css'; 5 | import TurndownService from 'turndown'; 6 | import { dummyUsers } from '../lib/dummy-data'; 7 | import { BugReportPreview } from './BugReportPreview'; 8 | 9 | const turndownService = new TurndownService(); 10 | 11 | const loggedInUser = dummyUsers[0]; 12 | 13 | interface BugReportEditorProps { 14 | onSave: (content: { title: string; content: string; tags: string[] }) => void; 15 | } 16 | 17 | export const BugReportEditor = ({ onSave }: BugReportEditorProps) => { 18 | const [title, setTitle] = useState(''); 19 | const [content, setContent] = useState(''); 20 | const [tags, setTags] = useState([]); 21 | const [newTag, setNewTag] = useState(''); 22 | const [isPreview, setIsPreview] = useState(false); 23 | 24 | const handleTitleChange = useCallback( 25 | (e: React.ChangeEvent) => { 26 | setTitle(e.target.value); 27 | }, 28 | [] 29 | ); 30 | 31 | const handleContentChange = useCallback((value: string) => { 32 | setContent(value); 33 | }, []); 34 | 35 | const handleNewTagChange = useCallback( 36 | (e: React.ChangeEvent) => { 37 | setNewTag(e.target.value); 38 | }, 39 | [] 40 | ); 41 | 42 | const handleAddTag = useCallback( 43 | (e: React.KeyboardEvent) => { 44 | if (e.key === 'Enter' && newTag.trim()) { 45 | e.preventDefault(); 46 | setTags((prevTags) => [...prevTags, newTag.trim()]); 47 | setNewTag(''); 48 | } 49 | }, 50 | [newTag] 51 | ); 52 | 53 | const handleRemoveTag = useCallback((indexToRemove: number) => { 54 | setTags((prevTags) => 55 | prevTags.filter((_, index) => index !== indexToRemove) 56 | ); 57 | }, []); 58 | 59 | const handleSave = useCallback(() => { 60 | if (!title.trim() || !content.trim()) return; 61 | 62 | onSave({ 63 | title, 64 | content: turndownService.turndown(content), 65 | tags, 66 | }); 67 | }, [title, content, tags, onSave]); 68 | 69 | const togglePreview = useCallback(() => { 70 | setIsPreview((prev) => !prev); 71 | }, []); 72 | 73 | return ( 74 |
75 |
76 | 83 | 84 |
85 |
86 | {tags.map((tag, index) => ( 87 | 91 | {tag} 92 | 98 | 99 | ))} 100 |
101 | 109 |
110 | {/* Toolbar */} 111 |
112 | 125 |
126 | 127 | {/* Content Editor / Preview */} 128 | {isPreview ? ( 129 | 140 | ) : ( 141 | 149 | )} 150 |
151 | 152 |
153 | 159 |
160 |
161 | ); 162 | }; 163 | -------------------------------------------------------------------------------- /src/components/BugReportPreview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ArrowLeft, Bug, Check, ThumbsUp } from 'lucide-react'; 3 | import { formatDistanceToNow } from 'date-fns'; 4 | import ReactMarkdown from 'react-markdown'; 5 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 6 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; 7 | import remarkGfm from 'remark-gfm'; 8 | import { dummyUsers } from '../lib/dummy-data'; 9 | 10 | interface BugReportPreviewProps { 11 | bugReport: { 12 | title: string; 13 | content: string; 14 | tags: string[]; 15 | upvotes: number; 16 | is_resolved: boolean; 17 | created_at: string; 18 | }; 19 | onBack: () => void; 20 | } 21 | 22 | export function BugReportPreview({ bugReport, onBack }: BugReportPreviewProps) { 23 | const author = dummyUsers[0]; 24 | 25 | return ( 26 |
27 | {/* Back Button */} 28 | 35 | 36 | {/* Author Info and Status */} 37 |
38 |
39 | {author?.username 44 |
45 |

46 | {author?.username || 'Unknown User'} 47 |

48 |

49 | {formatDistanceToNow(new Date(bugReport?.created_at || ''), { 50 | addSuffix: true, 51 | })} 52 |

53 |
54 |
55 |
56 | {bugReport?.is_resolved && ( 57 | 58 | 59 | Resolved 60 | 61 | )} 62 | 63 | 64 | Bug Report 65 | 66 |
67 |
68 | 69 | {/* Bug Title */} 70 |

71 | {bugReport?.title || ''} 72 |

73 | 74 | {/* Bug Content */} 75 |
76 | 88 | {String(children).replace(/\n$/, '')} 89 | 90 | ) : ( 91 | 95 | {children} 96 | 97 | ); 98 | }, 99 | ul: ({ node, ...props }) => ( 100 |
    101 | ), 102 | ol: ({ node, ...props }) => ( 103 |
      104 | ), 105 | li: ({ node, ...props }) =>
    1. , 106 | img: ({ node, ...props }) => ( 107 | 108 | ), 109 | a: ({ href, children }) => ( 110 | 116 | {children} 117 | 118 | ), 119 | }} 120 | > 121 | {bugReport.content} 122 | 123 |
124 | 125 | {/* Tags and Upvotes */} 126 |
127 |
128 | {bugReport?.tags?.map((tag, index) => ( 129 | 133 | {tag} 134 | 135 | ))} 136 |
137 | 141 |
142 | 143 | {bugReport.is_resolved && ( 144 |
145 |
146 | 147 | This issue has been resolved 148 |
149 |
150 | )} 151 |
152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '../components/Button'; 3 | import { BookOpen, Bug, MessageSquare } from 'lucide-react'; 4 | import { Link } from 'react-router-dom'; 5 | import { dummyArticles, dummyPosts } from '../lib/dummy-data'; 6 | import { formatDistanceToNow } from 'date-fns'; 7 | 8 | export function Home() { 9 | return ( 10 |
11 | {/* Hero Section */} 12 |
13 |

14 | Welcome to DevHub 15 |

16 |

17 | Join our thriving community of developers to collaborate, share 18 | knowledge, and grow together. 19 |

20 |
21 | 24 | 27 |
28 |
29 | 30 | {/* Features Section */} 31 |
32 |

33 | Everything You Need 34 |

35 |
36 | } 40 | /> 41 | } 45 | /> 46 | } 50 | /> 51 |
52 |
53 | 54 | {/* Latest Activity Section */} 55 |
56 |

57 | Latest Activity 58 |

59 |
60 |
61 |

Recent Discussions

62 |
63 | {dummyPosts.slice(0, 3).map((post) => ( 64 | 69 |

70 | {post.title} 71 |

72 |

73 | {formatDistanceToNow(new Date(post.created_at), { 74 | addSuffix: true, 75 | })} 76 |

77 | 78 | ))} 79 |
80 |
81 |
82 |

Latest Articles

83 |
84 | {dummyArticles.slice(0, 3).map((article) => ( 85 | 90 |

91 | {article.title} 92 |

93 |

94 | {article.description} 95 |

96 | 97 | ))} 98 |
99 |
100 |
101 |
102 | 103 | {/* Community Stats Section */} 104 |
105 |

Our Growing Community

106 |
107 | 108 | 109 | 110 | 111 |
112 |
113 | 114 | {/* CTA Section */} 115 |
116 |

Ready to Join?

117 |

118 | Start collaborating with developers from around the world. Join our 119 | community today! 120 |

121 | 124 |
125 |
126 | ); 127 | } 128 | 129 | function FeatureCard({ 130 | title, 131 | description, 132 | icon, 133 | }: { 134 | title: string; 135 | description: string; 136 | icon: React.ReactNode; 137 | }) { 138 | return ( 139 |
140 |
{icon}
141 |

142 | {title} 143 |

144 |

{description}

145 |
146 | ); 147 | } 148 | 149 | function StatCard({ number, label }: { number: string; label: string }) { 150 | return ( 151 |
152 |
{number}
153 |
{label}
154 |
155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /src/pages/Discussions.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useMemo } from 'react'; 2 | import { MessageSquare, Search, Filter } from 'lucide-react'; 3 | import { PostCard } from '../components/PostCard'; 4 | import { dummyPosts } from '../lib/dummy-data'; 5 | import { Button } from '../components/Button'; 6 | const CATEGORIES = ['Frontend', 'Backend', 'DevOps'] as const; 7 | 8 | export function Discussions() { 9 | const [searchTerm, setSearchTerm] = useState(''); 10 | const [selectedCategory, setSelectedCategory] = useState(null); 11 | const [showResolved, setShowResolved] = useState(null); 12 | 13 | const handleSearchChange = useCallback( 14 | (e: React.ChangeEvent) => { 15 | setSearchTerm(e.target.value); 16 | }, 17 | [] 18 | ); 19 | 20 | const handleCategoryChange = useCallback((category: string) => { 21 | setSelectedCategory((prev) => (prev === category ? null : category)); 22 | }, []); 23 | 24 | const handleResolvedChange = useCallback((isResolved: boolean) => { 25 | setShowResolved((prev) => (prev === isResolved ? null : isResolved)); 26 | }, []); 27 | 28 | const filteredPosts = useMemo(() => { 29 | return dummyPosts.filter((post) => { 30 | const matchesSearch = 31 | searchTerm === '' || 32 | post.title.toLowerCase().includes(searchTerm.toLowerCase()) || 33 | post.tags.some((tag) => 34 | tag.toLowerCase().includes(searchTerm.toLowerCase()) 35 | ); 36 | 37 | const matchesCategory = 38 | !selectedCategory || post.category === selectedCategory; 39 | const matchesResolved = 40 | showResolved === null || post.is_resolved === showResolved; 41 | 42 | return matchesSearch && matchesCategory && matchesResolved; 43 | }); 44 | }, [searchTerm, selectedCategory, showResolved]); 45 | 46 | const renderCategoryFilters = useCallback( 47 | () => ( 48 |
49 |

50 | Categories 51 |

52 |
53 | {CATEGORIES.map((category) => ( 54 | 65 | ))} 66 |
67 |
68 | ), 69 | [selectedCategory, handleCategoryChange] 70 | ); 71 | 72 | const renderStatusFilters = useCallback( 73 | () => ( 74 |
75 |

76 | Status 77 |

78 |
79 | 90 | 101 |
102 |
103 | ), 104 | [showResolved, handleResolvedChange] 105 | ); 106 | 107 | return ( 108 |
109 | {/* Header Section */} 110 |
111 |
112 |

113 | Discussions 114 |

115 |

116 | Join the conversation with fellow developers 117 |

118 |
119 | 122 |
123 | 124 | {/* Main Content */} 125 |
126 | {/* Filters Section */} 127 |
128 |
129 |
130 | 131 | Filters 132 |
133 |
134 | {renderCategoryFilters()} 135 | {renderStatusFilters()} 136 |
137 |
138 |
139 | 140 | {/* Posts Section */} 141 |
142 | {/* Search Bar */} 143 |
144 |
145 | 152 | 153 |
154 |
155 | 156 | {/* Posts List */} 157 |
158 | {filteredPosts.map((post) => ( 159 | 160 | ))} 161 | {filteredPosts.length === 0 && ( 162 |
163 | No discussions found matching your criteria 164 |
165 | )} 166 |
167 |
168 |
169 |
170 | ); 171 | } 172 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { 4 | Users, 5 | MessageSquare, 6 | BookOpen, 7 | Bug, 8 | Video, 9 | Pencil, 10 | Menu, 11 | X, 12 | Bell, 13 | } from 'lucide-react'; 14 | import { ThemeToggle } from './ThemeToggle'; 15 | import LOGO from '../assets/images/main-logo.svg'; 16 | 17 | interface NavLinkProps { 18 | to: string; 19 | Icon: React.ComponentType<{ className?: string }>; 20 | text: string; 21 | } 22 | 23 | interface MobileNavLinkProps extends NavLinkProps { 24 | setIsMobileMenuOpen: React.Dispatch>; 25 | } 26 | 27 | const NavLink: React.FC = ({ to, Icon, text }) => ( 28 | 32 | 33 | {text} 34 | 35 | ); 36 | 37 | const MobileNavLink: React.FC = ({ 38 | to, 39 | Icon, 40 | text, 41 | setIsMobileMenuOpen, 42 | }) => { 43 | const handleClick = useCallback(() => { 44 | setIsMobileMenuOpen(false); 45 | }, [setIsMobileMenuOpen]); 46 | 47 | return ( 48 | 53 | 54 | {text} 55 | 56 | ); 57 | }; 58 | 59 | const NAVIGATION_ITEMS = [ 60 | { to: '/discussions', Icon: MessageSquare, text: 'Discussions' }, 61 | { to: '/knowledge-base', Icon: BookOpen, text: 'Knowledge Base' }, 62 | { to: '/bug-reports', Icon: Bug, text: 'Bug Reports' }, 63 | { to: '/blog', Icon: Pencil, text: 'Blog' }, 64 | { to: '/meetings', Icon: Video, text: 'Meetings' }, 65 | ]; 66 | 67 | const MobileMenu: React.FC<{ 68 | isOpen: boolean; 69 | onClose: () => void; 70 | }> = ({ isOpen, onClose }) => { 71 | return ( 72 | <> 73 |
78 |
79 | 84 | DevHub Logo 85 |

86 | DEV 87 | HUB 88 |

89 | 90 | 96 |
97 | 98 |
99 | {NAVIGATION_ITEMS.map((item) => ( 100 | 105 | ))} 106 |
107 |
108 | {isOpen && ( 109 |
113 | )} 114 | 115 | ); 116 | }; 117 | 118 | export function Navbar() { 119 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 120 | 121 | const handleMobileMenuToggle = useCallback(() => { 122 | setIsMobileMenuOpen((prev) => !prev); 123 | }, []); 124 | 125 | const handleMobileMenuClose = useCallback(() => { 126 | setIsMobileMenuOpen(false); 127 | }, []); 128 | 129 | return ( 130 | // fixed inset-x-0 top-0 z-50 border-b border-slate-200/10 backdrop-blur // we need to add this property properly and make sure it doesn't effect on open side bar 131 | 185 | ); 186 | } 187 | 188 | export default Navbar; 189 | -------------------------------------------------------------------------------- /src/pages/auth/ResetPassword.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { Eye, EyeOff } from 'lucide-react'; // Import eye icons 4 | import { supabase } from '../../lib/supabase'; 5 | import LOGO from '../../assets/images/main-logo.svg'; 6 | import { Button } from '../../components/Button'; 7 | export function ResetPassword() { 8 | const navigate = useNavigate(); 9 | const [password, setPassword] = useState(''); 10 | const [confirmPassword, setConfirmPassword] = useState(''); 11 | const [showPassword, setShowPassword] = useState(false); 12 | const [showConfirmPassword, setShowConfirmPassword] = useState(false); 13 | const [loading, setLoading] = useState(false); 14 | const [error, setError] = useState(null); 15 | const [message, setMessage] = useState(null); 16 | const [passwordError, setPasswordError] = useState(null); 17 | 18 | // Validate password match in real-time 19 | const validatePasswordMatch = () => { 20 | if (password !== confirmPassword) { 21 | setPasswordError('Passwords do not match'); 22 | } else { 23 | setPasswordError(null); 24 | } 25 | }; 26 | 27 | useEffect(() => { 28 | if (password && confirmPassword) { 29 | if (password !== confirmPassword) { 30 | setPasswordError('Passwords do not match'); 31 | } else { 32 | setPasswordError(null); 33 | } 34 | } 35 | }, [password, confirmPassword]); 36 | 37 | const handleResetPassword = async (e: React.FormEvent) => { 38 | e.preventDefault(); 39 | setLoading(true); 40 | setError(null); 41 | setMessage(null); 42 | 43 | // Check if passwords match before proceeding 44 | if (password !== confirmPassword) { 45 | setPasswordError('Passwords do not match'); 46 | setLoading(false); 47 | return; 48 | } 49 | 50 | try { 51 | const { error } = await supabase.auth.updateUser({ password }); 52 | 53 | if (error) throw error; 54 | 55 | setMessage( 56 | 'Password updated successfully. Redirecting to sign-in page...' 57 | ); 58 | setTimeout(() => navigate('/signin'), 3000); 59 | } catch (err) { 60 | setError(err instanceof Error ? err.message : 'An error occurred'); 61 | } finally { 62 | setLoading(false); 63 | } 64 | }; 65 | 66 | return ( 67 |
68 |
69 |
70 | DevHub Logo 76 |

77 | DEV 78 | HUB 79 |

80 |
81 | 82 |
83 |

84 | Reset Password 85 |

86 |

87 | Enter your new password 88 |

89 |
90 | 91 | {error && ( 92 |
93 | {error} 94 |
95 | )} 96 | 97 | {message && ( 98 |
99 | {message} 100 |
101 | )} 102 | 103 |
104 |
105 | {/* New Password Field */} 106 |
107 | { 113 | setPassword(e.target.value); 114 | validatePasswordMatch(); // Validate on change 115 | }} 116 | className="w-full px-3 py-2 border border-[var(--border-color)] rounded bg-[var(--bg-primary)] text-[var(--text-primary)] focus:ring-[var(--accent)] focus:border-[var(--accent)]" 117 | placeholder="New Password" 118 | /> 119 | 130 |
131 | 132 | {/* Confirm Password Field */} 133 |
134 | { 140 | setConfirmPassword(e.target.value); 141 | validatePasswordMatch(); // Validate on change 142 | }} 143 | className="w-full px-3 py-2 border border-[var(--border-color)] rounded bg-[var(--bg-primary)] text-[var(--text-primary)] focus:ring-[var(--accent)] focus:border-[var(--accent)]" 144 | placeholder="Confirm Password" 145 | /> 146 | 157 |
158 | 159 | {/* Password Mismatch Error */} 160 | {passwordError && ( 161 |
{passwordError}
162 | )} 163 |
164 | 165 | 174 |
175 |
176 |
177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /src/pages/KnowledgeBase.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useMemo } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Book, Search, Tag, Eye, Calendar } from 'lucide-react'; 4 | import { dummyArticles } from '../lib/dummy-data'; 5 | import { formatDistanceToNow } from 'date-fns'; 6 | import { Button } from '../components/Button'; 7 | interface Article { 8 | id: string; 9 | title: string; 10 | description: string; 11 | category: string; 12 | tags: string[]; 13 | views: number; 14 | updated_at: string; 15 | } 16 | 17 | export function KnowledgeBase() { 18 | const [searchTerm, setSearchTerm] = useState(''); 19 | const [selectedCategory, setSelectedCategory] = useState(null); 20 | 21 | // Memoize categories to prevent unnecessary recalculation 22 | const categories = useMemo(() => { 23 | return Array.from( 24 | new Set(dummyArticles.map((article) => article.category)) 25 | ); 26 | }, []); 27 | 28 | // Extract search handler 29 | const handleSearch = useCallback((e: React.ChangeEvent) => { 30 | setSearchTerm(e.target.value); 31 | }, []); 32 | 33 | // Extract category selection handler 34 | const handleCategorySelect = useCallback((category: string) => { 35 | setSelectedCategory((prev) => (prev === category ? null : category)); 36 | }, []); 37 | 38 | // Memoize filtered articles 39 | const filteredArticles = useMemo(() => { 40 | return dummyArticles.filter((article) => { 41 | const matchesSearch = 42 | article.title.toLowerCase().includes(searchTerm.toLowerCase()) || 43 | article.description.toLowerCase().includes(searchTerm.toLowerCase()) || 44 | article.tags.some((tag) => 45 | tag.toLowerCase().includes(searchTerm.toLowerCase()) 46 | ); 47 | const matchesCategory = 48 | !selectedCategory || article.category === selectedCategory; 49 | return matchesSearch && matchesCategory; 50 | }); 51 | }, [searchTerm, selectedCategory]); 52 | 53 | // Extract category item renderer 54 | const renderCategoryItem = useCallback( 55 | (category: string) => ( 56 |
  • handleCategorySelect(category)} 64 | > 65 | {category} 66 |
  • 67 | ), 68 | [selectedCategory, handleCategorySelect] 69 | ); 70 | 71 | // Extract article renderer 72 | const renderArticle = useCallback( 73 | (article: Article) => ( 74 |
    78 | 79 |

    80 | {article.title} 81 |

    82 | 83 |

    84 | {article.description} 85 |

    86 |
    87 |
    88 | 89 |
    90 | {article.tags.map((tag) => ( 91 | 95 | {tag} 96 | 97 | ))} 98 |
    99 |
    100 |
    101 | 102 | 103 | {article.views} views 104 | 105 | 106 | 107 | {formatDistanceToNow(new Date(article.updated_at), { 108 | addSuffix: true, 109 | })} 110 | 111 |
    112 |
    113 |
    114 | ), 115 | [] 116 | ); 117 | 118 | return ( 119 |
    120 | {/* Header Section */} 121 |
    122 |
    123 |

    124 | Knowledge Base 125 |

    126 |

    127 | Explore our community-driven knowledge base of tech stacks and best 128 | practices. 129 |

    130 |
    131 | 134 |
    135 | 136 | {/* Main Content */} 137 |
    138 | {/* Filters Section */} 139 |
    140 |
    141 |

    142 | Categories 143 |

    144 |
      {categories.map(renderCategoryItem)}
    145 |
    146 |
    147 | 148 | {/* Articles Section */} 149 |
    150 | {/* Search Bar */} 151 |
    152 | 159 | 160 |
    161 | 162 | {/* Articles List */} 163 |
    164 | {filteredArticles.map(renderArticle)} 165 | {filteredArticles.length === 0 && ( 166 |
    167 | No articles found matching your criteria 168 |
    169 | )} 170 |
    171 |
    172 |
    173 |
    174 | ); 175 | } 176 | -------------------------------------------------------------------------------- /src/components/BlogEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from 'react'; 2 | import { Save, Image, Link as LinkIcon, Eye, Edit } from 'lucide-react'; 3 | import ReactQuill from 'react-quill'; 4 | import 'react-quill/dist/quill.snow.css'; 5 | import { BlogPreview } from './BlogPreview'; 6 | import TurndownService from 'turndown'; 7 | import { dummyUsers } from '../lib/dummy-data'; 8 | 9 | const turndownService = new TurndownService(); 10 | 11 | const loggedInUser = dummyUsers[0]; 12 | 13 | interface BlogEditorProps { 14 | onSave: (content: { 15 | title: string; 16 | content: string; 17 | tags: string[]; 18 | coverImage?: string; 19 | }) => void; 20 | } 21 | 22 | export function BlogEditor({ onSave }: BlogEditorProps) { 23 | const [title, setTitle] = useState(''); 24 | const [content, setContent] = useState(''); 25 | const [tags, setTags] = useState([]); 26 | const [newTag, setNewTag] = useState(''); 27 | const [coverImage, setCoverImage] = useState(null); 28 | const [isPreview, setIsPreview] = useState(false); 29 | 30 | const handleTitleChange = useCallback( 31 | (e: React.ChangeEvent) => { 32 | setTitle(e.target.value); 33 | }, 34 | [] 35 | ); 36 | 37 | const handleContentChange = useCallback((value: string) => { 38 | setContent(value); 39 | }, []); 40 | 41 | const handleNewTagChange = useCallback( 42 | (e: React.ChangeEvent) => { 43 | setNewTag(e.target.value); 44 | }, 45 | [] 46 | ); 47 | 48 | const handleAddTag = useCallback( 49 | (e: React.KeyboardEvent) => { 50 | if (e.key === 'Enter' && newTag.trim()) { 51 | e.preventDefault(); 52 | setTags((prevTags) => [...prevTags, newTag.trim()]); 53 | setNewTag(''); 54 | } 55 | }, 56 | [newTag] 57 | ); 58 | 59 | const handleRemoveTag = useCallback((indexToRemove: number) => { 60 | setTags((prevTags) => 61 | prevTags.filter((_, index) => index !== indexToRemove) 62 | ); 63 | }, []); 64 | 65 | const handleSave = useCallback(() => { 66 | if (!title.trim() || !content.trim()) return; 67 | 68 | onSave({ 69 | title, 70 | content, 71 | tags, 72 | coverImage: coverImage || undefined, 73 | }); 74 | }, [title, content, tags, coverImage, onSave]); 75 | 76 | const togglePreview = useCallback(() => { 77 | setIsPreview((prev) => !prev); 78 | }, []); 79 | 80 | const handleImageUpload = (event: React.ChangeEvent) => { 81 | if (event.target.files && event.target.files[0]) { 82 | const file = event.target.files[0]; 83 | const reader = new FileReader(); 84 | reader.onloadend = () => { 85 | setCoverImage(reader.result as string); 86 | }; 87 | reader.readAsDataURL(file); 88 | } 89 | }; 90 | 91 | return ( 92 |
    93 | {/* Title */} 94 | 101 | 102 | {/* Cover Image */} 103 | {coverImage && ( 104 | Cover 109 | )} 110 | 119 | 120 | {/* Tags */} 121 |
    122 |
    123 | {tags.map((tag, index) => ( 124 | 128 | {tag} 129 | 135 | 136 | ))} 137 |
    138 | 146 |
    147 | 148 | {/* Toolbar */} 149 |
    150 | 163 |
    164 | 165 | {/* Content Editor / Preview */} 166 | {isPreview ? ( 167 | 180 | ) : ( 181 |
    182 |
    183 | 191 |
    192 |
    193 | )} 194 | 195 | {/* Save Button */} 196 |
    197 | 203 |
    204 |
    205 | ); 206 | } 207 | -------------------------------------------------------------------------------- /src/pages/BugReports.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useMemo } from 'react'; 2 | import { Bug, Search, Filter } from 'lucide-react'; 3 | import { Link } from 'react-router-dom'; 4 | import { dummyBugReports } from '../lib/dummy-data'; 5 | import { BugReportCard } from '../components/BugReportCard'; 6 | import { Button } from '../components/Button'; 7 | export function BugReports() { 8 | const [searchTerm, setSearchTerm] = useState(''); 9 | const [selectedTags, setSelectedTags] = useState([]); 10 | const [showResolved, setShowResolved] = useState(null); 11 | 12 | const allTags = useMemo(() => { 13 | return Array.from(new Set(dummyBugReports.flatMap((bug) => bug.tags))); 14 | }, []); 15 | 16 | const handleSearchChange = useCallback( 17 | (e: React.ChangeEvent) => { 18 | setSearchTerm(e.target.value); 19 | }, 20 | [] 21 | ); 22 | 23 | const toggleTag = useCallback((tag: string) => { 24 | setSelectedTags((prev) => 25 | prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag] 26 | ); 27 | }, []); 28 | 29 | const handleResolvedToggle = useCallback((value: boolean) => { 30 | setShowResolved((prev) => (prev === value ? null : value)); 31 | }, []); 32 | 33 | const matchesSearchTerm = useCallback( 34 | (bug: (typeof dummyBugReports)[0]) => { 35 | const searchLower = searchTerm.toLowerCase(); 36 | return ( 37 | bug.title.toLowerCase().includes(searchLower) || 38 | bug.content.toLowerCase().includes(searchLower) || 39 | bug.tags.some((tag) => tag.toLowerCase().includes(searchLower)) 40 | ); 41 | }, 42 | [searchTerm] 43 | ); 44 | 45 | const matchesTags = useCallback( 46 | (bug: (typeof dummyBugReports)[0]) => { 47 | return ( 48 | selectedTags.length === 0 || 49 | selectedTags.every((tag) => bug.tags.includes(tag)) 50 | ); 51 | }, 52 | [selectedTags] 53 | ); 54 | 55 | const matchesResolved = useCallback( 56 | (bug: (typeof dummyBugReports)[0]) => { 57 | return showResolved === null || bug.is_resolved === showResolved; 58 | }, 59 | [showResolved] 60 | ); 61 | 62 | const filteredBugs = useMemo(() => { 63 | return dummyBugReports.filter( 64 | (bug) => 65 | matchesSearchTerm(bug) && matchesTags(bug) && matchesResolved(bug) 66 | ); 67 | }, [matchesSearchTerm, matchesTags, matchesResolved]); 68 | 69 | return ( 70 |
    71 |
    72 |
    73 |

    74 | Bug Reports 75 |

    76 |

    77 | Report and track bugs, get help from the community 78 |

    79 |
    80 | 83 |
    84 | 85 | {/* Main Content */} 86 |
    87 | {/* Filters Section */} 88 |
    89 |
    90 |
    91 | 92 | Filters 93 |
    94 |
    95 | {/* Status Filters */} 96 |
    97 |

    98 | Status 99 |

    100 |
    101 | 112 | 123 |
    124 |
    125 | 126 | {/* Tags Filters */} 127 |
    128 |

    129 | Tags 130 |

    131 |
    132 | {allTags.map((tag) => ( 133 | 144 | ))} 145 |
    146 |
    147 |
    148 |
    149 |
    150 | 151 | {/* Bug Reports Section */} 152 |
    153 | {/* Search Bar */} 154 |
    155 |
    156 | 163 | 164 |
    165 |
    166 | 167 | {/* Bug Reports List */} 168 |
    169 | {filteredBugs.map((bug) => ( 170 | 171 | ))} 172 | {filteredBugs.length === 0 && ( 173 |
    174 | No bug reports found matching your criteria 175 |
    176 | )} 177 |
    178 |
    179 |
    180 |
    181 | ); 182 | } 183 | -------------------------------------------------------------------------------- /src/pages/DiscussionDetail.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react'; 2 | import { useNavigate, useParams } from 'react-router-dom'; 3 | import { MessageSquare, ThumbsUp, Check, ArrowLeft } from 'lucide-react'; 4 | import { formatDistanceToNow } from 'date-fns'; 5 | import { dummyPosts, dummyComments, dummyUsers } from '../lib/dummy-data'; 6 | 7 | const CommentItem = React.memo( 8 | ({ comment }: { comment: (typeof dummyComments)[0] }) => ( 9 |
    10 | {comment.user.username} 15 |
    16 |
    17 |
    18 |

    {comment.user.username}

    19 | 20 | {formatDistanceToNow(new Date(comment.created_at), { 21 | addSuffix: true, 22 | })} 23 | 24 |
    25 |

    {comment.content}

    26 |
    27 |
    28 | 32 | {comment.is_accepted && ( 33 | 34 | 35 | Accepted Answer 36 | 37 | )} 38 |
    39 |
    40 |
    41 | ) 42 | ); 43 | 44 | const PostTag = React.memo(({ tag }: { tag: string }) => ( 45 | 46 | {tag} 47 | 48 | )); 49 | 50 | export function DiscussionDetail() { 51 | const navigate = useNavigate(); 52 | const { id } = useParams<{ id: string }>(); 53 | 54 | const { post, author, comments } = useMemo(() => { 55 | const foundPost = dummyPosts.find((p) => p.id === id); 56 | const foundAuthor = dummyUsers.find((u) => u.id === foundPost?.user_id); 57 | const relatedComments = dummyComments.filter((c) => c.post_id === id); 58 | 59 | return { 60 | post: foundPost, 61 | author: foundAuthor, 62 | comments: relatedComments, 63 | }; 64 | }, [id]); 65 | 66 | // Event handlers 67 | const handleNavigateBack = useCallback(() => { 68 | navigate('/discussions'); 69 | }, [navigate]); 70 | 71 | const handleCommentSubmit = useCallback((e: React.FormEvent) => { 72 | e.preventDefault(); 73 | // Add comment submission logic here 74 | }, []); 75 | 76 | if (!post || !author) { 77 | return
    Discussion not found
    ; 78 | } 79 | 80 | return ( 81 |
    82 |
    83 | 90 |
    91 |
    92 | {/* Post Section */} 93 |
    94 | {/* Author Info and Resolved Status */} 95 |
    96 |
    97 | {author.username} 102 |
    103 |

    {author.username}

    104 |

    105 | {formatDistanceToNow(new Date(post.created_at), { 106 | // please wrap this in useMemo 107 | addSuffix: true, 108 | })} 109 |

    110 |
    111 |
    112 | {post.is_resolved && ( 113 | 114 | 115 | Resolved 116 | 117 | )} 118 |
    119 | 120 | {/* Post Title */} 121 |

    {post.title}

    122 | 123 | {/* Post Content */} 124 |
    125 |

    {post.content}

    126 |
    127 | 128 | {/* Tags and Upvotes */} 129 |
    130 |
    131 | {post.tags.map((tag) => ( 132 | 133 | ))} 134 |
    135 | 139 |
    140 | 141 | {/* Comments Section */} 142 |
    143 |

    144 | {comments.length} {comments.length === 1 ? 'Comment' : 'Comments'} 145 |

    146 | 147 | {/* Comments List */} 148 |
    149 | {comments.map((comment) => ( 150 | 151 | ))} 152 |
    153 | 154 | {/* Comment Form */} 155 |
    156 |