diff --git a/package-lock.json b/package-lock.json
index 9b9ae4b5..c08bdc9d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,7 @@
"lucide-react": "^0.552.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
- "react-router-dom": "^7.9.5",
+ "react-router-dom": "^7.11.0",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
@@ -2293,12 +2293,16 @@
"license": "MIT"
},
"node_modules/cookie": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
- "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": {
@@ -2920,9 +2924,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3582,9 +3586,9 @@
}
},
"node_modules/react-router": {
- "version": "7.9.5",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
- "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==",
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz",
+ "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -3604,12 +3608,12 @@
}
},
"node_modules/react-router-dom": {
- "version": "7.9.5",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz",
- "integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==",
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz",
+ "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==",
"license": "MIT",
"dependencies": {
- "react-router": "7.9.5"
+ "react-router": "7.11.0"
},
"engines": {
"node": ">=20.0.0"
diff --git a/package.json b/package.json
index 8e30fcd7..210f0bca 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"lucide-react": "^0.552.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
- "react-router-dom": "^7.9.5",
+ "react-router-dom": "^7.11.0",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
diff --git a/src/App.tsx b/src/App.tsx
index 4b1419ad..a2fc4051 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,4 @@
-import { Routes, Route } from 'react-router'
+import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Home from './pages/Home.tsx'
import CreatePostPage from './pages/CreatePostPage.tsx'
import Navbar from './components/Navbar.tsx'
@@ -8,6 +8,7 @@ import CreateCommunityPage from './pages/CreateCommunityPage.tsx'
import {CommunityPage} from './pages/CommunityPage.tsx'
import { CommunitiesPage } from './pages/CommunitiesPage.tsx'
import MessagesPage from './pages/MessagesPage.tsx'
+import NotFound from './components/NotFound.tsx' // Import the 404 page
import EventsPage from './pages/EventsPage.tsx'
import EventDetailPage from './pages/EventDetailPage.tsx'
import CreateEventPage from './pages/CreateEventPage.tsx'
@@ -21,6 +22,26 @@ import ProfilePage from './pages/ProfilePage.tsx'
function App() {
return (
+ <>
+
+
{/* Wrap everything with Router */}
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ {/* Add 404 route at the end - catches all undefined routes */}
+ } />
+
+
+
+
+ >
@@ -54,4 +75,4 @@ function App() {
)
}
-export default App
+export default App
\ No newline at end of file
diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx
new file mode 100644
index 00000000..0e7b48e0
--- /dev/null
+++ b/src/components/CodeBlock.tsx
@@ -0,0 +1,65 @@
+// src/components/CodeBlock.tsx
+import { useState } from 'react';
+import { Copy, Check } from 'lucide-react';
+
+interface CodeBlockProps {
+ code: string;
+ language?: string;
+}
+
+export const CodeBlock = ({ code, language = '' }: CodeBlockProps) => {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(code);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error('Failed to copy: ', err);
+ const textArea = document.createElement('textarea');
+ textArea.value = code;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ return (
+
+ {/* Header with language and copy button */}
+
+ {language && (
+
+ {language}
+
+ )}
+
+
+
+ {/* Code block */}
+
+ {code}
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/CommunityDisplay.tsx b/src/components/CommunityDisplay.tsx
index c25e57b1..ec79e739 100644
--- a/src/components/CommunityDisplay.tsx
+++ b/src/components/CommunityDisplay.tsx
@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import PostItem from "./PostItem";
+import PostSkeleton from "./PostSkeleton"; // Import the skeleton component
import { fetchCommunityPost } from "../utils/communityApi";
interface Props {
@@ -27,8 +28,25 @@ const CommunityDisplay = ({ communityId }: Props) => {
queryFn: () => fetchCommunityPost(communityId),
});
- if (isLoading)
- return Loading posts...
;
+ // Skeleton loading state
+ if (isLoading) {
+ return (
+
+ {/* Skeleton header */}
+
+
+ {/* Skeleton posts */}
+
+ {[...Array(3)].map((_, index) => (
+
+ ))}
+
+
+ );
+ }
if (error)
return (
@@ -52,7 +70,6 @@ const CommunityDisplay = ({ communityId }: Props) => {
{data && data.length > 0 ? (
-
{data.map((post) => (
diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx
new file mode 100644
index 00000000..0393595f
--- /dev/null
+++ b/src/components/CopyButton.tsx
@@ -0,0 +1,66 @@
+// src/components/CopyButton.tsx
+import { useState } from 'react';
+import { Copy, Check } from 'lucide-react';
+
+interface CopyButtonProps {
+ text: string;
+ className?: string;
+ size?: 'sm' | 'md' | 'lg';
+}
+
+export const CopyButton = ({ text, className = '', size = 'md' }: CopyButtonProps) => {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error('Failed to copy text: ', err);
+ // Fallback for older browsers
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ const sizeClasses = {
+ sm: 'px-2 py-1 text-xs',
+ md: 'px-3 py-1.5 text-sm',
+ lg: 'px-4 py-2 text-base'
+ };
+
+ const iconSize = {
+ sm: 'w-3 h-3',
+ md: 'w-4 h-4',
+ lg: 'w-5 h-5'
+ };
+
+ return (
+
+ );
+};
+
+export default CopyButton;
\ No newline at end of file
diff --git a/src/components/NotFound.tsx b/src/components/NotFound.tsx
new file mode 100644
index 00000000..c34362f2
--- /dev/null
+++ b/src/components/NotFound.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+const NotFound = () => {
+ const styles = {
+ container: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ minHeight: '100vh',
+ backgroundColor: '#f5f5f5',
+ padding: '20px',
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
+ } as React.CSSProperties,
+ content: {
+ textAlign: 'center' as const,
+ backgroundColor: 'white',
+ padding: '40px',
+ borderRadius: '12px',
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.1)',
+ maxWidth: '500px',
+ width: '100%'
+ } as React.CSSProperties,
+ title: {
+ fontSize: '6rem',
+ fontWeight: 700,
+ color: '#dc3545',
+ margin: 0,
+ lineHeight: 1
+ } as React.CSSProperties,
+ subtitle: {
+ fontSize: '2rem',
+ fontWeight: 600,
+ color: '#333',
+ margin: '20px 0 10px'
+ } as React.CSSProperties,
+ message: {
+ fontSize: '1.1rem',
+ color: '#666',
+ marginBottom: '30px',
+ lineHeight: '1.5'
+ } as React.CSSProperties,
+ button: {
+ display: 'inline-block',
+ backgroundColor: '#007bff',
+ color: 'white',
+ padding: '12px 30px',
+ borderRadius: '6px',
+ textDecoration: 'none',
+ fontWeight: 500,
+ fontSize: '1rem',
+ transition: 'background-color 0.3s ease'
+ } as React.CSSProperties
+ };
+
+ return (
+
+
+
404
+
Page Not Found
+
+ Oops! The page you're looking for doesn't exist or has been moved.
+
+
+ Return to Feed
+
+
+
+ );
+};
+
+export default NotFound;
\ No newline at end of file
diff --git a/src/components/PostDetail.tsx b/src/components/PostDetail.tsx
index bcdb03a1..94b6e149 100644
--- a/src/components/PostDetail.tsx
+++ b/src/components/PostDetail.tsx
@@ -1,10 +1,12 @@
-import { useState } from 'react';
+import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { supabase } from '../supabase-client';
-import { MessageCircle, Send, Bookmark, ArrowLeft } from 'lucide-react';
+import { MessageCircle, Send, Bookmark, ArrowLeft, Code, Copy, Check } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import LikeButton from './LikeButton';
import CommentSection from './CommentSection';
+import CopyButton from './CopyButton';
+import { CodeBlock } from './CodeBlock';
interface Post {
id: number;
@@ -19,6 +21,73 @@ interface PostDetailProps {
postId: number;
}
+// Helper function to parse content and extract code blocks
+const parseContentWithCode = (content: string) => {
+ // Regex to match code blocks with optional language specifier
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
+ const parts = [];
+ let lastIndex = 0;
+ let match;
+
+ // Reset regex lastIndex
+ codeBlockRegex.lastIndex = 0;
+
+ while ((match = codeBlockRegex.exec(content)) !== null) {
+ // Add text before code block
+ if (match.index > lastIndex) {
+ parts.push({
+ type: 'text',
+ content: content.substring(lastIndex, match.index)
+ });
+ }
+
+ // Add code block
+ parts.push({
+ type: 'code',
+ language: match[1] || '',
+ content: match[2].trim()
+ });
+
+ lastIndex = match.index + match[0].length;
+ }
+
+ // Add remaining text after last code block
+ if (lastIndex < content.length) {
+ parts.push({
+ type: 'text',
+ content: content.substring(lastIndex)
+ });
+ }
+
+ // If no code blocks were found, return the entire content as text
+ if (parts.length === 0) {
+ return [{ type: 'text', content }];
+ }
+
+ return parts;
+};
+
+// Helper function to count code blocks
+const countCodeBlocks = (content: string): number => {
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
+ const matches = content.match(codeBlockRegex);
+ return matches ? matches.length : 0;
+};
+
+// Helper function to extract all code snippets for bulk copy
+const extractAllCodeSnippets = (content: string): string => {
+ const codeBlockRegex = /```(?:\w+)?\n([\s\S]*?)```/g;
+ const snippets: string[] = [];
+ let match;
+
+ codeBlockRegex.lastIndex = 0;
+ while ((match = codeBlockRegex.exec(content)) !== null) {
+ snippets.push(match[1].trim());
+ }
+
+ return snippets.join('\n\n' + '-'.repeat(50) + '\n\n');
+};
+
const fetchPost = async (postId: number): Promise
=> {
const { data, error } = await supabase
.from('Posts')
@@ -35,12 +104,34 @@ const fetchPost = async (postId: number): Promise => {
const PostDetail = ({ postId }: PostDetailProps) => {
const navigate = useNavigate();
const [likeCount, setLikeCount] = useState(0);
+ const [showAllCode, setShowAllCode] = useState(false);
const { data: post, error, isLoading } = useQuery({
queryKey: ["post", postId],
queryFn: () => fetchPost(postId)
});
+ // Parse content for code blocks
+ const contentParts = useMemo(() =>
+ post ? parseContentWithCode(post.content) : [],
+ [post]
+ );
+
+ const hasCodeBlocks = useMemo(() =>
+ contentParts.some(part => part.type === 'code'),
+ [contentParts]
+ );
+
+ const codeBlocksCount = useMemo(() =>
+ post ? countCodeBlocks(post.content) : 0,
+ [post]
+ );
+
+ const allCodeSnippets = useMemo(() =>
+ post ? extractAllCodeSnippets(post.content) : '',
+ [post]
+ );
+
if (isLoading) {
return (
@@ -86,69 +177,158 @@ const PostDetail = ({ postId }: PostDetailProps) => {
{/* Back Button */}
{/* Post Card */}
-
+
{/* Header: Avatar and Title */}
-
+
{post.avatar_url ? (

) : (
-
+
)}
-
-
{post.title}
-
{formatDate(post.created_at)}
+
+
{post.title}
+
{formatDate(post.created_at)}
+
+ {/* Code blocks indicator */}
+ {hasCodeBlocks && (
+
+
+
+ {codeBlocksCount} code snippet{codeBlocksCount !== 1 ? 's' : ''}
+
+
+ )}
{/* Image */}
{post.image_url && (
-
+
)}
+ {/* Copy All Code Button (if has code blocks) */}
+ {hasCodeBlocks && allCodeSnippets && (
+
+
+
+
+
Code Snippets
+
+ {codeBlocksCount} total
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Full Content with Code Blocks */}
+
+
+ {contentParts.map((part, index) => {
+ if (part.type === 'code') {
+ return (
+
+
+ {showAllCode && index < contentParts.length - 1 && (
+
+ )}
+
+ );
+ }
+
+ // Render text content
+ if (part.content.trim() === '') return null;
+
+ return (
+
+
+ {part.content}
+
+ {showAllCode && index < contentParts.length - 1 && part.type === 'text' && (
+
+ )}
+
+ );
+ })}
+
+
+
{/* Action Buttons */}
-
+
+
+
+
+ {likeCount}
+
+
+
+
-
-
-
+ {hasCodeBlocks && (
+
+ )}
+
-
{/* Likes Count */}
-
-
+
+
{likeCount} {likeCount === 1 ? 'like' : 'likes'}
- {/* Full Content */}
-
-
- {post.title}
- {post.content}
-
-
-
{/* Comments Section */}
-
+
+
+
Comments
+
Join the discussion about this post
+
diff --git a/src/components/PostItem.tsx b/src/components/PostItem.tsx
index 126a6805..6a24fb6a 100644
--- a/src/components/PostItem.tsx
+++ b/src/components/PostItem.tsx
@@ -1,77 +1,194 @@
import { Link } from 'react-router-dom';
import type { Post } from './PostList';
-import { MessageCircle, Heart } from 'lucide-react';
+import { MessageCircle, Heart, Code } from 'lucide-react';
import LikeButton from './LikeButton';
-import { useState } from 'react';
+import { useState, useMemo } from 'react';
+import { CopyButton } from './CopyButton';
+import { CodeBlock } from './CodeBlock';
interface Props {
- post: Post;
+ post: Post;
}
+// Helper function to detect and extract code blocks from content
+const parseContentWithCode = (content: string) => {
+ // Regex to match code blocks with optional language specifier
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
+ const parts = [];
+ let lastIndex = 0;
+ let match;
+
+ // Reset regex lastIndex
+ codeBlockRegex.lastIndex = 0;
+
+ while ((match = codeBlockRegex.exec(content)) !== null) {
+ // Add text before code block
+ if (match.index > lastIndex) {
+ parts.push({
+ type: 'text',
+ content: content.substring(lastIndex, match.index)
+ });
+ }
+
+ // Add code block
+ parts.push({
+ type: 'code',
+ language: match[1] || '',
+ content: match[2].trim()
+ });
+
+ lastIndex = match.index + match[0].length;
+ }
+
+ // Add remaining text after last code block
+ if (lastIndex < content.length) {
+ parts.push({
+ type: 'text',
+ content: content.substring(lastIndex)
+ });
+ }
+
+ // If no code blocks were found, return the entire content as text
+ if (parts.length === 0) {
+ return [{ type: 'text', content }];
+ }
+
+ return parts;
+};
+
+// Helper to extract first code snippet for preview
+const extractFirstCodeSnippet = (content: string): string | null => {
+ const codeBlockRegex = /```(?:\w+)?\n([\s\S]*?)```/;
+ const match = content.match(codeBlockRegex);
+ return match ? match[1].trim().slice(0, 100) + (match[1].trim().length > 100 ? '...' : '') : null;
+};
+
const PostItem = ({ post }: Props) => {
- const [likeCount, setLikeCount] = useState(0);
+ const [likeCount, setLikeCount] = useState(0);
+
+ // Parse content to detect code blocks
+ const contentParts = useMemo(() => parseContentWithCode(post.content), [post.content]);
+
+ // Check if content has code blocks
+ const hasCodeBlocks = useMemo(() =>
+ contentParts.some(part => part.type === 'code'),
+ [contentParts]
+ );
+
+ // Extract first code snippet for preview
+ const firstCodeSnippet = useMemo(() =>
+ extractFirstCodeSnippet(post.content),
+ [post.content]
+ );
- return (
-
-
- {/* Image */}
- {post.image_url && (
-
-

-
-
- )}
+ return (
+
+
+ {/* Image */}
+ {post.image_url && (
+
+

+
+
+ )}
- {/* Content */}
-
- {/* Header with Avatar */}
-
- {post.avatar_url ? (
-

- ) : (
-
- )}
-
-
- {post.title}
-
-
2h ago
-
-
+ {/* Content */}
+
+ {/* Header with Avatar */}
+
+ {post.avatar_url ? (
+

+ ) : (
+
+ )}
+
+
+ {post.title}
+
+
2h ago
+
+
- {/* Title and Content Preview */}
-
-
- {post.content.length > 150 ? post.content.slice(0, 150) + '...' : post.content}
-
-
+ {/* Title and Content Preview */}
+
+ {/* Show code snippet preview if available */}
+ {hasCodeBlocks && firstCodeSnippet && (
+
+
+
+ {firstCodeSnippet}
+
+
+ )}
+
+ {/* Show text content preview */}
+ {contentParts.filter(part => part.type === 'text').map((part, index) => {
+ if (part.content.trim() === '') return null;
+
+ const textContent = part.content.length > 150
+ ? part.content.slice(0, 150) + '...'
+ : part.content;
+
+ return (
+
+ {textContent}
+
+ );
+ })}
+
+ {/* Show code block count badge */}
+ {hasCodeBlocks && (
+
+
+
+ {contentParts.filter(part => part.type === 'code').length} code snippet(s)
+
+
+ )}
+
- {/* Stats and Actions */}
-
-
-
-
- {likeCount}
-
-
-
- 0
-
-
-
-
+ {/* Stats and Actions */}
+
+
+
+
+ {likeCount}
+
+
+
+ 0
+
+ {hasCodeBlocks && (
+
+
+ Code
+ )}
-
- );
+
+
+
+
+
+ );
}
export default PostItem;
\ No newline at end of file
diff --git a/src/components/PostSkeleton.tsx b/src/components/PostSkeleton.tsx
new file mode 100644
index 00000000..29320131
--- /dev/null
+++ b/src/components/PostSkeleton.tsx
@@ -0,0 +1,44 @@
+// src/components/PostSkeleton.tsx
+import React from 'react';
+
+const PostSkeleton: React.FC = () => {
+ return (
+
+ {/* Header with user info */}
+
+
+ {/* Post title */}
+
+
+ {/* Post content lines */}
+
+
+ {/* Tags */}
+
+
+ {/* Action buttons */}
+
+
+ );
+};
+
+export default PostSkeleton;
\ No newline at end of file