├── 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 |
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 |
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 |
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 |
45 | {currentThemeIcon}
46 |
47 |
48 | {/* Dropdown Menu */}
49 | {isOpen && (
50 |
54 | {themes.map((t) => (
55 | handleThemeChange(t.value)}
63 | >
64 | {t.icon}
65 | {t.label}
66 |
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 |
28 |
29 | {count}
30 |
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 |
26 |
27 | {count}
28 |
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 |
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 |
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 |
62 |
{participant.name}
63 |
64 | ),
65 | []
66 | );
67 |
68 | return (
69 |
70 | {isCallActive ? (
71 |
72 | ) : (
73 |
74 |
75 |
Meeting Room
76 |
80 |
81 | Join Call
82 |
83 |
84 |
85 |
86 |
87 |
88 | Participants ({participants.length})
89 |
90 |
91 | {participants.map(renderParticipant)}
92 |
93 |
94 |
95 |
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 |
77 |
84 |
85 |
86 |
87 |
95 | {isVideoEnabled ? (
96 |
97 | ) : (
98 |
99 | )}
100 |
101 |
109 | {isAudioEnabled ? (
110 |
111 | ) : (
112 |
113 | )}
114 |
115 |
119 |
120 |
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 |
34 |
35 | Back to Editor
36 |
37 |
38 | {/* Cover Image */}
39 | {blog.cover_image && (
40 |
45 | )}
46 |
47 | {/* Author & Date */}
48 |
49 |
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 }) => ,
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 |
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 |
125 |
126 |
127 |
or continue with
128 |
134 | Sign up with GitHub
135 |
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 |
83 |
84 | Back to Knowledge Base
85 |
86 |
87 |
88 |
89 | {/* Author Info and Views */}
90 |
91 |
92 |
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 |
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 |
133 |
134 |
135 |
or continue with
136 |
142 | Sign in with GitHub
143 |
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 |
140 | {content}
141 |
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 |
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 |
{' '}
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 |
navigate('/blog')}
29 | className="inline-flex items-center text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
30 | >
31 |
32 | Back to Blogs
33 |
34 |
35 |
36 | {/* Blog Post Section */}
37 |
38 | {/* Cover Image */}
39 |
44 |
45 | {/* Blog Post Content */}
46 |
47 | {/* Author Info and Metadata */}
48 |
49 |
50 |
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 |
84 |
90 | WELCOME TO DEVHUB COMMUNITY
91 |
92 |
93 | );
94 | }
95 |
96 | return (
97 |
98 |
99 |
100 |
103 |
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 |
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 | handleRemoveTag(index)}
94 | className="ml-2 text-[var(--text-secondary)]"
95 | >
96 | ×
97 |
98 |
99 | ))}
100 |
101 |
109 |
110 | {/* Toolbar */}
111 |
112 |
116 | {isPreview ? (
117 |
118 | ) : (
119 |
120 |
Preview Bug Report before post
121 |
122 |
123 | )}
124 |
125 |
126 |
127 | {/* Content Editor / Preview */}
128 | {isPreview ? (
129 |
140 | ) : (
141 |
149 | )}
150 |
151 |
152 |
153 |
157 | Save
158 |
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 |
32 |
33 | Back to Editor
34 |
35 |
36 | {/* Author Info and Status */}
37 |
38 |
39 |
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 }) => ,
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 |
138 |
139 | {bugReport?.upvotes || 0}
140 |
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 |
22 | Join Community
23 |
24 |
25 | Explore Resources
26 |
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 |
122 | Get Started
123 |
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 |
55 | handleCategoryChange(category)}
59 | className="rounded text-indigo-600"
60 | />
61 |
62 | {category}
63 |
64 |
65 | ))}
66 |
67 |
68 | ),
69 | [selectedCategory, handleCategoryChange]
70 | );
71 |
72 | const renderStatusFilters = useCallback(
73 | () => (
74 |
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 |
120 | New Discussion
121 |
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 |
85 |
86 | DEV
87 | HUB
88 |
89 |
90 |
94 |
95 |
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 |
132 |
133 |
134 | {/* Left Sidebar (Logo & Desktop Menu) */}
135 |
136 |
137 |
143 |
144 | DEV
145 | HUB
146 |
147 |
148 |
149 | {NAVIGATION_ITEMS.map((item) => (
150 |
151 | ))}
152 |
153 |
154 |
155 | {/* Right Side (Icons & Mobile Menu Button) */}
156 |
157 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | {/* Mobile Menu Button */}
169 |
173 | {isMobileMenuOpen ? (
174 |
175 | ) : (
176 |
177 | )}
178 |
179 |
180 |
181 |
182 |
183 |
184 |
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 |
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 |
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 |
132 | Write Article
133 |
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 |
109 | )}
110 |
111 | Upload Cover Image
112 |
118 |
119 |
120 | {/* Tags */}
121 |
122 |
123 | {tags.map((tag, index) => (
124 |
128 | {tag}
129 | handleRemoveTag(index)}
131 | className="ml-2 text-[var(--text-secondary)]"
132 | >
133 | ×
134 |
135 |
136 | ))}
137 |
138 |
146 |
147 |
148 | {/* Toolbar */}
149 |
150 |
154 | {isPreview ? (
155 |
156 | ) : (
157 |
158 |
Preview Blog before post
159 |
160 |
161 | )}
162 |
163 |
164 |
165 | {/* Content Editor / Preview */}
166 | {isPreview ? (
167 |
180 | ) : (
181 |
193 | )}
194 |
195 | {/* Save Button */}
196 |
197 |
201 | Save
202 |
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 |
81 | Report Bug
82 |
83 |
84 |
85 | {/* Main Content */}
86 |
87 | {/* Filters Section */}
88 |
89 |
90 |
91 |
92 | Filters
93 |
94 |
95 | {/* Status Filters */}
96 |
125 |
126 | {/* Tags Filters */}
127 |
128 |
129 | Tags
130 |
131 |
132 | {allTags.map((tag) => (
133 |
134 | toggleTag(tag)}
138 | className="rounded text-[var(--accent)]"
139 | />
140 |
141 | {tag}
142 |
143 |
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 |
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 |
29 |
30 | {comment.upvotes}
31 |
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 |
87 |
88 | Back to Discussions
89 |
90 |
91 |
92 | {/* Post Section */}
93 |
94 | {/* Author Info and Resolved Status */}
95 |
96 |
97 |
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 |
136 |
137 | {post.upvotes}
138 |
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 |
171 |
172 |
173 |
174 |
175 | );
176 | }
177 |
--------------------------------------------------------------------------------
/src/pages/Blog.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useCallback, useMemo } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Pencil, Search, Calendar, Eye, Heart } from 'lucide-react';
4 | import { dummyBlogPosts } from '../lib/dummy-data';
5 | import { formatDistanceToNow } from 'date-fns';
6 | import type { BlogPost } from '../types';
7 | import { Button } from '../components/Button';
8 | interface TagButtonProps {
9 | tag: string;
10 | isSelected: boolean;
11 | onTagSelect: (tag: string) => void;
12 | }
13 |
14 | const TagButton = ({ tag, isSelected, onTagSelect }: TagButtonProps) => (
15 | onTagSelect(tag)}
17 | className={`block w-full text-left px-2 py-1 rounded ${
18 | isSelected
19 | ? 'bg-[var(--accent)]/10 text-[var(--accent)]'
20 | : 'text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)]'
21 | }`}
22 | >
23 | {tag}
24 |
25 | );
26 |
27 | interface BlogPostCardProps {
28 | post: BlogPost;
29 | }
30 |
31 | const BlogPostCard = ({ post }: BlogPostCardProps) => (
32 |
33 |
38 |
39 |
40 |
41 | {post.title}
42 |
43 |
44 |
45 | {/* Author Info */}
46 |
47 |
52 |
53 |
54 | {post.author.username}
55 |
56 |
57 |
58 | {formatDistanceToNow(new Date(post.created_at), {
59 | addSuffix: true,
60 | })}
61 |
62 |
63 |
64 |
65 | {/* Metadata */}
66 |
67 |
68 |
69 |
70 | {post.views}
71 |
72 |
73 |
74 | {/* {post.likes} */}
75 |
76 |
77 |
78 | {post.tags.map((tag) => (
79 |
83 | {tag}
84 |
85 | ))}
86 |
87 |
88 |
89 |
90 | );
91 |
92 | export function Blog() {
93 | const [searchTerm, setSearchTerm] = useState('');
94 | const [selectedTag, setSelectedTag] = useState(null);
95 |
96 | const allTags = useMemo(() => {
97 | return Array.from(new Set(dummyBlogPosts.flatMap((post) => post.tags)));
98 | }, []);
99 |
100 | const handleSearchChange = useCallback(
101 | (event: React.ChangeEvent) => {
102 | setSearchTerm(event.target.value);
103 | },
104 | []
105 | );
106 |
107 | const handleTagSelect = useCallback((tag: string) => {
108 | setSelectedTag((prevTag) => (prevTag === tag ? null : tag));
109 | }, []);
110 |
111 | const filteredPosts = useMemo(() => {
112 | return dummyBlogPosts.filter((post) => {
113 | const matchesSearch =
114 | post.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
115 | post.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
116 | post.tags.some((tag) =>
117 | tag.toLowerCase().includes(searchTerm.toLowerCase())
118 | );
119 |
120 | const matchesTag = !selectedTag || post.tags.includes(selectedTag);
121 |
122 | return matchesSearch && matchesTag;
123 | });
124 | }, [searchTerm, selectedTag]);
125 |
126 | const renderEmptyState = () => (
127 |
128 | No blog posts found matching your criteria
129 |
130 | );
131 |
132 | const renderTagsList = () => (
133 |
134 | {allTags.map((tag) => (
135 |
141 | ))}
142 |
143 | );
144 |
145 | const renderSearchBar = () => (
146 |
147 |
154 |
155 |
156 | );
157 |
158 | return (
159 |
160 | {/* Header Section */}
161 |
162 |
163 |
164 | Blog
165 |
166 |
167 | Share your knowledge and experiences
168 |
169 |
170 |
171 | Write Post
172 |
173 |
174 |
175 | {/* Main Content */}
176 |
177 | {/* Filters Section */}
178 |
179 |
180 |
181 | Popular Tags
182 |
183 | {renderTagsList()}
184 |
185 |
186 |
187 | {/* Blog Posts Section */}
188 |
189 |
{renderSearchBar()}
190 |
191 | {/* Blog Posts Grid */}
192 |
193 | {filteredPosts.map((post) => (
194 |
195 | ))}
196 | {filteredPosts.length === 0 && renderEmptyState()}
197 |
198 |
199 |
200 |
201 | );
202 | }
203 |
--------------------------------------------------------------------------------
/src/pages/Notifications.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Bell,
4 | Check,
5 | X,
6 | Calendar,
7 | MessageSquare,
8 | Heart,
9 | Star,
10 | AlertCircle,
11 | ArrowLeft,
12 | } from 'lucide-react';
13 | import { formatDistanceToNow } from 'date-fns';
14 | import { useNavigate } from 'react-router-dom';
15 |
16 | interface Notification {
17 | id: string;
18 | type: 'comment' | 'like' | 'mention' | 'follow' | 'system';
19 | title: string;
20 | message: string;
21 | timestamp: string;
22 | read: boolean;
23 | link: string;
24 | }
25 |
26 | // Dummy notifications data with different types
27 | const dummyNotifications: Notification[] = [
28 | {
29 | id: '1',
30 | type: 'comment',
31 | title: 'New Comment',
32 | message:
33 | 'John Doe commented on your post "Best practices for React performance optimization"',
34 | timestamp: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
35 | read: false,
36 | link: '/discussions/1',
37 | },
38 | {
39 | id: '2',
40 | type: 'like',
41 | title: 'Post Liked',
42 | message: 'Your post received 10 likes',
43 | timestamp: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
44 | read: false,
45 | link: '/blog/1',
46 | },
47 | {
48 | id: '3',
49 | type: 'mention',
50 | title: 'Mentioned in Discussion',
51 | message: 'Alice mentioned you in "Docker networking issues"',
52 | timestamp: new Date(Date.now() - 1000 * 60 * 60).toISOString(),
53 | read: true,
54 | link: '/discussions/2',
55 | },
56 | {
57 | id: '4',
58 | type: 'system',
59 | title: 'System Update',
60 | message: 'New features have been added to the platform',
61 | timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(),
62 | read: true,
63 | link: '/announcements',
64 | },
65 | {
66 | id: '5',
67 | type: 'follow',
68 | title: 'New Follower',
69 | message: 'Sarah started following you',
70 | timestamp: new Date(Date.now() - 1000 * 60 * 60 * 3).toISOString(),
71 | read: false,
72 | link: '/profile/sarah',
73 | },
74 | ];
75 |
76 | export function Notifications() {
77 | const navigate = useNavigate();
78 |
79 | const [notifications, setNotifications] =
80 | useState(dummyNotifications);
81 | const [filter, setFilter] = useState<'all' | 'unread'>('all');
82 |
83 | const markAsRead = (id: string) => {
84 | setNotifications((prev) =>
85 | prev.map((n) => (n.id === id ? { ...n, read: true } : n))
86 | );
87 | };
88 |
89 | const markAllAsRead = () => {
90 | setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
91 | };
92 |
93 | const deleteNotification = (id: string) => {
94 | setNotifications((prev) => prev.filter((n) => n.id !== id));
95 | };
96 |
97 | const filteredNotifications = notifications.filter(
98 | (n) => filter === 'all' || !n.read
99 | );
100 |
101 | const getNotificationIcon = (type: Notification['type']) => {
102 | switch (type) {
103 | case 'comment':
104 | return ;
105 | case 'like':
106 | return ;
107 | case 'mention':
108 | return ;
109 | case 'follow':
110 | return ;
111 | case 'system':
112 | return ;
113 | }
114 | };
115 |
116 | return (
117 |
118 |
119 |
navigate('/')}
121 | className="inline-flex items-center text-sm text-gray-400 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-200"
122 | >
123 |
124 | Back
125 |
126 |
127 |
128 |
129 | {/* Header */}
130 |
131 |
132 |
133 | Notifications
134 |
135 |
136 |
139 | setFilter(e.target.value as 'all' | 'unread')
140 | }
141 | className="rounded-md ml-3 border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 border-[var(--border-color)] bg-[var(--card-bg)] "
142 | >
143 | All
144 | Unread
145 |
146 |
150 | Mark all as read
151 |
152 |
153 |
154 |
155 |
156 | {/* Notifications List */}
157 |
158 | {filteredNotifications.length === 0 ? (
159 |
160 | No notifications to display
161 |
162 | ) : (
163 | filteredNotifications.map((notification) => (
164 |
170 |
171 |
172 |
179 | {getNotificationIcon(notification.type)}
180 |
181 |
182 |
183 | {notification.title}
184 |
185 |
186 | {notification.message}
187 |
188 |
189 | {formatDistanceToNow(
190 | new Date(notification.timestamp),
191 | { addSuffix: true }
192 | )}
193 |
194 |
195 |
196 |
197 | {!notification.read && (
198 | markAsRead(notification.id)}
200 | className="p-1 text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
201 | title="Mark as read"
202 | >
203 |
204 |
205 | )}
206 | deleteNotification(notification.id)}
208 | className="p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400"
209 | title="Delete notification"
210 | >
211 |
212 |
213 |
214 |
215 |
216 | ))
217 | )}
218 |
219 |
220 |
221 |
222 | );
223 | }
224 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | 
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
DevHub Community Platform
12 |
13 | Devhub is a web app that is A modern community platform for developers to Connect, collaborate, and grow with fellow developers.
14 |
15 | Report Bug
16 | ·
17 | Known Issues
18 |
19 |
20 |
21 |
22 | ## What is it?
23 |
24 | 
25 |
26 | ## ✨ Features
27 |
28 | - 💬 **Discussions** - Engage in meaningful conversations about programming
29 | - 📚 **Knowledge Base** - Community-driven documentation and guides
30 | - 🐛 **Bug Reports** - Track and solve issues together
31 | - 📝 **Blog Platform** - Share your insights and experiences
32 | - 🎥 **Virtual Meetings** - Real-time collaboration spaces
33 | - 🌙 **Multiple Themes** - Light, Dark, and other beautiful themes
34 | - 🔒 **Authentication** - Secure user authentication with custom backend
35 | - 🎨 **Modern UI** - Beautiful and responsive design with Tailwind CSS
36 |
37 | ## 🛠️ Tech Stack
38 |
39 | - **Frontend:**
40 |
41 | - React 18
42 | - TypeScript
43 | - Tailwind CSS
44 | - Vite
45 | - Lucide Icons
46 |
47 | - **Backend:**
48 | - Node.js
49 | - Express.js
50 | - Custom authentication system
51 | - REST API
52 |
53 | ## 🔗 Contributor Guidelines
54 |
55 | Before contributing to DevHub, please follow these steps:
56 |
57 | ### 1️⃣ Contact Me for Slack & Jira Access 📩
58 |
59 | To be added to **Slack** and **Jira**, you need to send me an email first.
60 |
61 | - **Email:** usamaaamirsohail@gmail.com
62 | - **LinkedIn:** [Contact Me Here](https://www.linkedin.com/in/usama-aamir-0434b6229/)
63 |
64 | Once added, you can proceed to the next steps.
65 |
66 | ### 2️⃣ Create a Jira Ticket 📌
67 |
68 | - After being added, create a **ticket in Jira** with a clear description of your feature or issue.
69 | - Provide relevant details, screenshots, or references if needed.
70 |
71 | ### 3️⃣ Work on a New Branch 🚀
72 |
73 | - After approval, **create a new branch** for your changes.
74 | - Implement the enhancement and commit your changes.
75 |
76 | ### 4️⃣ Submit a PR for Review ✅
77 |
78 | - Once done, **create a Pull Request (PR)** and assign it for review.
79 | - The changes will be reviewed and merged upon approval.
80 |
81 | Thank you for contributing to DevHub! 🚀
82 |
83 | ## 🚀 Getting Started
84 |
85 | ### Prerequisites
86 |
87 | - Node.js 18+
88 | - npm or yarn
89 |
90 | ### Installation
91 |
92 | 1. Clone the repository:
93 |
94 | ```bash
95 | git clone https://github.com/usama7365/Devhub.git
96 | cd devhub
97 | ```
98 |
99 | 2. Install dependencies:
100 |
101 | ```bash
102 | npm install
103 | ```
104 |
105 | 3. Start the development server:
106 |
107 | ```bash
108 | npm run dev
109 | ```
110 |
111 | ## 🏗️ Project Structure
112 |
113 | ```
114 | src/
115 | ├── components/ # Reusable UI components
116 | ├── pages/ # Page components
117 | ├── lib/ # Utilities and configurations
118 | ├── types/ # TypeScript type definitions
119 | └── main.tsx # Application entry point
120 | ```
121 |
122 | ## 🤝 Contributing
123 |
124 | We welcome contributions! Please follow these steps:
125 |
126 | 1. Fork the repository
127 | 2. Create a new branch: `git checkout -b feature/amazing-feature`
128 | 3. Make your changes
129 | 4. Commit your changes: `git commit -m 'Add amazing feature'`
130 | 5. Push to the branch: `git push origin feature/amazing-feature`
131 | 6. Open a Pull Request
132 |
133 | ## 📝 License
134 |
135 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
136 |
137 | ## 🌟 Core Features
138 |
139 | ### Discussions
140 |
141 | - Create and participate in technical discussions
142 | - Tag-based categorization
143 | - Upvoting system
144 | - Mark solutions as accepted
145 |
146 | ### Knowledge Base
147 |
148 | - Community-driven documentation
149 | - Categorized articles
150 | - Search functionality
151 | - Version history
152 |
153 | ### Bug Reports
154 |
155 | - Structured bug reporting
156 | - Status tracking
157 | - Solution sharing
158 | - Integration with discussions
159 |
160 | ### Blog Platform
161 |
162 | - Rich text editor
163 | - Image uploads
164 | - Tags and categories
165 | - Social sharing
166 |
167 | ### Virtual Meetings
168 |
169 | - Real-time video conferencing
170 | - Screen sharing
171 | - Chat functionality
172 | - Meeting scheduling
173 |
174 | ## 🎨 Theme Support
175 |
176 | DevHub supports multiple themes:
177 |
178 | - Light
179 | - Dark
180 | - Sepia
181 | - Nord
182 | - Dracula
183 | - Ayu Mirage
184 | - Solarized Light
185 | - Solarized Dark
186 |
187 | ## 📱 Responsive Design
188 |
189 | The platform is fully responsive and works seamlessly across:
190 |
191 | - Desktop
192 | - Tablet
193 | - Mobile devices
194 |
195 | ## 🔒 Security
196 |
197 | - Custom authentication system (JWT)
198 | - Protected API routes
199 | - Secure data handling
200 |
201 | ## 🔄 Real-time Features
202 |
203 | - Live notifications
204 | - Real-time chat
205 | - Instant updates
206 | - Presence indicators
207 |
208 | ## 📈 Future Roadmap
209 |
210 | - [ ] Advanced code editor integration
211 | - [ ] GitHub integration
212 | - [ ] Team collaboration features
213 | - [ ] API documentation
214 | - [ ] Community events calendar
215 | - [ ] Developer portfolios
216 | - [ ] Job board integration
217 | - [ ] Mentorship program
218 |
219 | ## 💖 Acknowledgments
220 |
221 | - [Node.js](https://nodejs.org/en) for backend infrastructure
222 | - [Express.js](https://expressjs.com) for the server framework
223 | - [Tailwind CSS](https://tailwindcss.com) for styling
224 | - [Lucide](https://lucide.dev) for beautiful icons
225 | - [React](https://reactjs.org) for the UI framework
226 | - [Vite](https://vitejs.dev) for the build tool
227 |
228 | ## 💸 Sponsorship
229 |
230 | If you appreciate the work I'm doing on DevHub and want to support the development of the platform, consider becoming a sponsor.
231 |
232 | ### 💖 Sponsor Links:
233 |
234 | - **GitHub Sponsors:** [usama7365](https://github.com/sponsors/usama7365)
235 | - **Patreon:** [feline411](https://www.patreon.com/feline411)
236 |
237 | Thank you for supporting the DevHub community! 🙏
238 |
239 | ## 📧 Contact
240 |
241 | For questions or support, please open an issue or contact the maintainers:
242 |
243 | - GitHub: [GitHub Profile](https://github.com/usama7365)
244 | - Email: usamaaamirsohail@gmail.com
245 |
--------------------------------------------------------------------------------
/src/pages/BugReportDetail.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { useNavigate, useParams } from 'react-router-dom';
3 | import { Bug, ThumbsUp, Check, ArrowLeft } from 'lucide-react';
4 | import { formatDistanceToNow } from 'date-fns';
5 | import { dummyBugReports, dummyComments, dummyUsers } from '../lib/dummy-data';
6 | import ReactMarkdown, { Components } 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 | interface CodeProps {
11 | node: any;
12 | inline: boolean;
13 | className: string;
14 | children: React.ReactNode;
15 | [key: string]: any;
16 | }
17 |
18 | const CodeBlock: React.FC = ({
19 | inline,
20 | className,
21 | children,
22 | ...props
23 | }) => {
24 | const match = /language-(\w+)/.exec(className || '');
25 |
26 | if (!inline && match) {
27 | return (
28 |
34 | {String(children).replace(/\n$/, '')}
35 |
36 | );
37 | }
38 |
39 | return (
40 |
41 | {children}
42 |
43 | );
44 | };
45 |
46 | const markdownComponents: Components = {
47 | code: CodeBlock as any,
48 | };
49 |
50 | export function BugReportDetail() {
51 | const navigate = useNavigate();
52 | const { id } = useParams<{ id: string }>();
53 |
54 | const { bug, author, comments } = useMemo(() => {
55 | const foundBug = dummyBugReports.find((b) => b.id === id);
56 | const foundAuthor = dummyUsers.find((u) => u.id === foundBug?.user_id);
57 | const relatedComments = dummyComments.filter((c) => c.post_id === id);
58 |
59 | return {
60 | bug: foundBug,
61 | author: foundAuthor,
62 | comments: relatedComments,
63 | };
64 | }, [id]);
65 |
66 | const handleNavigateBack = () => {
67 | navigate('/bug-reports');
68 | };
69 |
70 | const handleCommentSubmit = (e: React.FormEvent) => {
71 | e.preventDefault();
72 | // will handle comments through socket
73 | };
74 |
75 | if (!bug || !author) {
76 | return (
77 |
78 | Bug report not found
79 |
80 | );
81 | }
82 |
83 | return (
84 |
85 |
86 |
90 |
91 | Back to Bug reports
92 |
93 |
94 |
95 | {/* Bug Report Section */}
96 |
97 | {/* Author Info and Status */}
98 |
99 |
100 |
105 |
106 |
107 | {author.username}
108 |
109 |
110 | {formatDistanceToNow(new Date(bug.created_at), {
111 | addSuffix: true,
112 | })}
113 |
114 |
115 |
116 |
117 | {bug.is_resolved && (
118 |
119 |
120 | Resolved
121 |
122 | )}
123 |
124 |
125 | Bug Report
126 |
127 |
128 |
129 |
130 | {/* Bug Title */}
131 |
132 | {bug.title}
133 |
134 |
135 | {/* Bug Content */}
136 |
137 |
138 | {bug.content}
139 |
140 |
141 |
142 | {/* Tags and Upvotes */}
143 |
144 |
145 | {bug.tags.map((tag) => (
146 |
150 | {tag}
151 |
152 | ))}
153 |
154 |
155 |
156 | {bug.upvotes}
157 |
158 |
159 |
160 | {/* Comments Section */}
161 |
162 |
163 | {comments.length} {comments.length === 1 ? 'Comment' : 'Comments'}
164 |
165 |
166 | {/* Comments List */}
167 |
168 | {comments.map((comment) => (
169 |
170 |
175 |
176 |
177 |
178 |
179 | {comment.user.username}
180 |
181 |
182 | {formatDistanceToNow(new Date(comment.created_at), {
183 | addSuffix: true,
184 | })}
185 |
186 |
187 |
188 | {comment.content}
189 |
190 |
191 |
192 |
193 |
194 | {comment.upvotes}
195 |
196 | {comment.is_accepted && (
197 |
198 |
199 | Solution
200 |
201 | )}
202 |
203 |
204 |
205 | ))}
206 |
207 |
208 | {/* Comment Form */}
209 |
210 |
215 |
216 |
220 | Post Comment
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 | );
229 | }
230 |
--------------------------------------------------------------------------------
/src/pages/Meetings.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from 'react';
2 | import { MeetingRoom } from '../components/MeetingRoom';
3 | import { Button } from '../components/Button';
4 | import {
5 | Video,
6 | Users,
7 | Calendar,
8 | Globe,
9 | Shield,
10 | Zap,
11 | Clock,
12 | Monitor,
13 | } from 'lucide-react';
14 | import { supabase } from '../lib/supabase';
15 | import { format, parseISO } from 'date-fns';
16 | import { dummyMeeting } from '../lib/dummy-data';
17 | import { Meeting } from '../types';
18 |
19 | const sampleParticipants = [
20 | {
21 | id: '1',
22 | name: 'John Doe',
23 | avatar: 'https://ui-avatars.com/api/?name=John+Doe&background=random',
24 | },
25 | {
26 | id: '2',
27 | name: 'Jane Smith',
28 | avatar: 'https://ui-avatars.com/api/?name=Jane+Smith&background=random',
29 | },
30 | {
31 | id: '3',
32 | name: 'feline Doe',
33 | avatar: 'https://ui-avatars.com/api/?name=feline+Doe&background=random',
34 | },
35 | {
36 | id: '4',
37 | name: 'usama Smith',
38 | avatar: 'https://ui-avatars.com/api/?name=usama+Smith&background=random',
39 | },
40 | {
41 | id: '5',
42 | name: 'code Doe',
43 | avatar: 'https://ui-avatars.com/api/?name=code+Doe&background=random',
44 | },
45 | {
46 | id: '6',
47 | name: 'alex Smith',
48 | avatar: 'https://ui-avatars.com/api/?name=alex+Smith&background=random',
49 | },
50 | ];
51 |
52 | const MEETING_SELECT_QUERY = `
53 | id,
54 | title,
55 | description,
56 | start_time,
57 | duration,
58 | max_participants,
59 | room_id,
60 | host:users (
61 | username,
62 | avatar_url
63 | )
64 | `;
65 |
66 | // Meeting Card
67 | interface MeetingCardProps {
68 | meeting: Meeting;
69 | onJoin: (roomId: string) => void;
70 | }
71 |
72 | const MeetingCard: React.FC = React.memo(
73 | ({ meeting, onJoin }) => {
74 | return (
75 |
76 |
77 |
{meeting.title}
78 |
{meeting.description}
79 |
80 |
81 |
86 |
87 |
88 | Hosted by {meeting.host.username}
89 |
90 |
{meeting.host.email}
91 |
92 |
93 |
94 |
95 |
96 |
97 | {format(parseISO(meeting.start_time), 'MMM d, h:mm a')}
98 |
99 |
100 |
101 | {meeting.max_participants} participants
102 |
103 |
104 |
105 |
onJoin(meeting.room_id)}
110 | >
111 | Join Meeting
112 |
113 |
114 |
115 | );
116 | }
117 | );
118 |
119 | function FeatureCard({
120 | icon,
121 | title,
122 | description,
123 | }: {
124 | icon: React.ReactNode;
125 | title: string;
126 | description: string;
127 | }) {
128 | return (
129 |
130 |
131 | {icon}
132 |
133 |
{title}
134 |
{description}
135 |
136 | );
137 | }
138 |
139 | export function Meetings() {
140 | const [selectedRoom, setSelectedRoom] = useState(null);
141 | const [upcomingMeetings, setUpcomingMeetings] = useState([]);
142 | const [loading, setLoading] = useState(true);
143 |
144 | const fetchMeetings = useCallback(async () => {
145 | try {
146 | const { data: meetings, error } = await supabase
147 | .from('meetings')
148 | .select(MEETING_SELECT_QUERY)
149 | .gte('start_time', new Date().toISOString())
150 | .order('start_time', { ascending: true });
151 |
152 | if (error) throw error;
153 |
154 | const processedMeetings =
155 | meetings.length > 0 ? meetings.map(processMeetingData) : [dummyMeeting];
156 |
157 | setUpcomingMeetings(processedMeetings);
158 | } catch (error) {
159 | console.error('Error fetching meetings:', error);
160 | setUpcomingMeetings([dummyMeeting]);
161 | } finally {
162 | setLoading(false);
163 | }
164 | }, []);
165 |
166 | useEffect(() => {
167 | fetchMeetings();
168 | }, [fetchMeetings]);
169 |
170 | const processMeetingData = useCallback(
171 | (meeting: any): Meeting => ({
172 | ...meeting,
173 | host: {
174 | ...meeting.host[0],
175 | id: meeting.host[0]?.id || '',
176 | username: meeting.host[0]?.username || '',
177 | email: meeting.host[0]?.email || '',
178 | created_at: meeting.host[0]?.created_at || '',
179 | },
180 | }),
181 | []
182 | );
183 |
184 | // Extract invite handling logic
185 | const handleInvite = useCallback(
186 | async (email: string) => {
187 | if (!selectedRoom) return;
188 |
189 | try {
190 | const { error } = await supabase.from('meeting_invitations').insert([
191 | {
192 | meeting_id: selectedRoom,
193 | email,
194 | },
195 | ]);
196 |
197 | if (error) throw error;
198 | } catch (error) {
199 | console.error('Error sending invitation:', error);
200 | }
201 | },
202 | [selectedRoom]
203 | );
204 |
205 | // Extract room selection handler
206 | const handleRoomSelect = useCallback((roomId: string) => {
207 | setSelectedRoom(roomId);
208 | }, []);
209 |
210 | // Extract meeting card rendering logic
211 | const renderMeetingCard = useCallback(
212 | (meeting: Meeting) => (
213 |
218 | ),
219 | [handleRoomSelect]
220 | );
221 |
222 | return (
223 |
224 | {/* Hero Section */}
225 |
226 |
227 |
232 |
233 |
234 |
235 |
236 | Virtual Meeting Spaces
237 |
238 |
239 | Connect with developers worldwide in our high-quality, secure
240 | virtual meeting rooms. Perfect for code reviews, pair programming,
241 | and team collaborations.
242 |
243 |
244 |
245 |
246 | {/* Upcoming Meetings */}
247 |
248 | Upcoming Meetings
249 |
250 | {loading ? (
251 | Loading...
252 | ) : upcomingMeetings.length > 0 ? (
253 |
254 | {upcomingMeetings.map(renderMeetingCard)}
255 |
256 | ) : (
257 |
258 | No upcoming meetings. Why not schedule one?
259 |
260 | )}
261 |
262 |
263 | {/* Active Meeting Room */}
264 | {selectedRoom && (
265 |
272 | )}
273 |
274 | {/* Schedule Section */}
275 |
276 |
277 |
Schedule a Meeting
278 |
279 | Plan ahead and schedule meetings with your team. Send automatic
280 | invitations and reminders.
281 |
282 |
283 | Schedule Now
284 |
285 |
286 |
287 | {/* Features Grid */}
288 |
289 |
290 | Meeting Features
291 |
292 |
293 | }
295 | title="Global Access"
296 | description="Connect with developers from any timezone, anywhere in the world."
297 | />
298 | }
300 | title="Secure Rooms"
301 | description="End-to-end encrypted video calls for maximum privacy and security."
302 | />
303 | }
305 | title="Low Latency"
306 | description="High-performance video and audio for smooth collaboration."
307 | />
308 | }
310 | title="Screen Sharing"
311 | description="Share your screen for code reviews and pair programming."
312 | />
313 |
314 |
315 |
316 | );
317 | }
318 |
--------------------------------------------------------------------------------