From 9cb703e7924e6e049cd6f22a32199eeb67c48e24 Mon Sep 17 00:00:00 2001 From: Saptarshi Mukherjee Date: Fri, 9 Jan 2026 19:24:27 +0530 Subject: [PATCH 1/4] feat: Add custom DevConnect favicon - Replace default Vite favicon with custom DevConnect design - Create devconnect-favicon.svg with network connectivity theme - Update index.html to reference new favicon - Improve page title for better branding --- index.html | 4 ++-- public/devconnect-favicon.svg | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 public/devconnect-favicon.svg diff --git a/index.html b/index.html index 181eb57c..8c2f16a8 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - devconnect + DevConnect - Connect, Collaborate, Create
diff --git a/public/devconnect-favicon.svg b/public/devconnect-favicon.svg new file mode 100644 index 00000000..4fe7fb4a --- /dev/null +++ b/public/devconnect-favicon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + From d6a4b01edff8ce09ad3486f21f0e9a525b3358f8 Mon Sep 17 00:00:00 2001 From: Saptarshi Mukherjee Date: Wed, 14 Jan 2026 21:47:55 +0530 Subject: [PATCH 2/4] feat: add peer-based skill endorsement system --- README.md | 45 ++++- SKILL_ENDORSEMENT.md | 291 +++++++++++++++++++++++++++++++ database-schema-skills.sql | 39 +++++ src/components/SkillsSection.tsx | 150 ++++++++++++++++ src/hooks/useSkills.ts | 120 +++++++++++++ src/pages/ProfilePage.tsx | 5 + src/types/skills.ts | 18 ++ 7 files changed, 665 insertions(+), 3 deletions(-) create mode 100644 SKILL_ENDORSEMENT.md create mode 100644 database-schema-skills.sql create mode 100644 src/components/SkillsSection.tsx create mode 100644 src/hooks/useSkills.ts create mode 100644 src/types/skills.ts diff --git a/README.md b/README.md index a2957896..e44c5c25 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ DevConnect is a full-stack web application that enables developers to: - ๐Ÿ” **GitHub Authentication** - Sign in with GitHub account ,Gmail based authentication - ๐Ÿ“ **Create Posts** - Share posts with images and content - ๐Ÿ‘ค **Profile Dashboard** - View user details, email, account info, and manage sessions +- ๐ŸŽฏ **Skill Endorsements** - Add skills to profile and receive peer endorsements - ๐Ÿ’ฌ **Nested Comments** - Multi-level comment threads with collapse/expand - ๐Ÿ‘ฅ **Communities** - Create and manage developer communities - โค๏ธ **Likes System** - Vote on posts and comments @@ -103,7 +104,8 @@ src/ โ”‚ โ”œโ”€โ”€ CreateEventForm.tsx # Event creation form โ”‚ โ”œโ”€โ”€ EventFilters.tsx # Event filtering controls โ”‚ โ”œโ”€โ”€ AttendeeList.tsx # Event attendees display -โ”‚ โ””โ”€โ”€ EventActions.tsx # Event interaction buttons +โ”‚ โ”œโ”€โ”€ EventActions.tsx # Event interaction buttons +โ”‚ โ””โ”€โ”€ SkillsSection.tsx # Skills display and endorsement โ”œโ”€โ”€ pages/ โ”‚ โ”œโ”€โ”€ Home.tsx # Home page โ”‚ โ”œโ”€โ”€ PostPage.tsx # Post detail page @@ -119,10 +121,13 @@ src/ โ”‚ โ”œโ”€โ”€ AuthContext.tsx # Authentication context | โ””โ”€โ”€ ThemeContext.tsx # Dark/light theme context โ”œโ”€โ”€ hooks/ -โ”‚ โ””โ”€โ”€ useMessaging.ts # Messaging-related hooks +โ”‚ โ”œโ”€โ”€ useMessaging.ts # Messaging-related hooks +โ”‚ โ”œโ”€โ”€ useEvents.ts # Event-related hooks +โ”‚ โ””โ”€โ”€ useSkills.ts # Skill endorsement hooks โ”œโ”€โ”€ types/ โ”‚ โ”œโ”€โ”€ messaging.ts # TypeScript interfaces for messaging -โ”‚ โ””โ”€โ”€ events.ts # TypeScript interfaces for events +โ”‚ โ”œโ”€โ”€ events.ts # TypeScript interfaces for events +โ”‚ โ””โ”€โ”€ skills.ts # TypeScript interfaces for skills โ”œโ”€โ”€ supabase-client.ts # Supabase configuration โ”œโ”€โ”€ theme.css # Theme-related global styles โ”œโ”€โ”€ App.tsx # Main app component @@ -267,6 +272,28 @@ CREATE TABLE EventAttendees ( For the complete messaging schema including conversations, messages, reactions, and real-time features, see `database-schema-messaging.sql`. +**Skills Tables** + +```sql +CREATE TABLE Skills ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + skill_name TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(user_id, skill_name) +); + +CREATE TABLE SkillEndorsements ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + skill_id BIGINT NOT NULL REFERENCES Skills(id) ON DELETE CASCADE, + endorser_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(skill_id, endorser_id) +); +``` + +For the complete skills schema with RLS policies, see `database-schema-skills.sql`. + **Storage Setup** - Create a bucket named `post-images` in Supabase Storage - Create a bucket named `message-files` in Supabase Storage (private) @@ -317,6 +344,15 @@ Quick setup: 2. Create the `event-images` storage bucket (public) 3. Navigate to `/events` to start creating events! +## ๐ŸŽฏ Setting Up Skill Endorsements + +For detailed instructions on setting up the skill endorsement system, see [SKILL_ENDORSEMENT.md](SKILL_ENDORSEMENT.md). + +Quick setup: +1. Run the SQL schema from `database-schema-skills.sql` +2. Skills appear automatically on user profiles +3. Visit `/profile` to add your skills and get endorsed! + ## ๐Ÿค Contributing We welcome contributions! Here's how to get started: @@ -457,6 +493,9 @@ Shows community listings and posts within communities. - **Events** โ†’ **Communities** (community_id): Many-to-One (optional) - **Events** โ†’ **EventAttendees**: One-to-Many - **EventAttendees** โ†’ **Users**: Many-to-One +- **Users** โ†’ **Skills** (1:N) +- **Skills** โ†’ **SkillEndorsements** (1:N) +- **Users** โ†’ **SkillEndorsements** (1:N) ### Query Patterns diff --git a/SKILL_ENDORSEMENT.md b/SKILL_ENDORSEMENT.md new file mode 100644 index 00000000..8119cfd2 --- /dev/null +++ b/SKILL_ENDORSEMENT.md @@ -0,0 +1,291 @@ +# Skill Endorsement System + +## Overview + +The Skill Endorsement System allows developers to showcase their skills on their profiles and receive endorsements from other community members, building trust and credibility within the DevConnect platform. + +## Features + +- โœ… Add/remove skills to your profile +- โœ… Endorse skills of other developers +- โœ… View endorsement counts for each skill +- โœ… Prevent self-endorsements +- โœ… One endorsement per skill per user +- โœ… Real-time updates using TanStack Query + +## Database Schema + +### Tables + +#### Skills Table +Stores user skills with unique constraint to prevent duplicate skills per user. + +```sql +CREATE TABLE Skills ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + skill_name TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(user_id, skill_name) +); +``` + +#### SkillEndorsements Table +Tracks endorsements with unique constraint to prevent multiple endorsements. + +```sql +CREATE TABLE SkillEndorsements ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + skill_id BIGINT NOT NULL REFERENCES Skills(id) ON DELETE CASCADE, + endorser_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(skill_id, endorser_id) +); +``` + +### Relationships + +- **Users** โ†’ **Skills** (1:N) - A user can have multiple skills +- **Skills** โ†’ **SkillEndorsements** (1:N) - A skill can have multiple endorsements +- **Users** โ†’ **SkillEndorsements** (1:N) - A user can endorse multiple skills + +### Security + +Row Level Security (RLS) policies ensure: +- Anyone can view skills and endorsements +- Users can only add/delete their own skills +- Users can only endorse others' skills (not their own) +- Users can remove their own endorsements + +## Setup Instructions + +### 1. Database Setup + +Run the SQL schema in your Supabase SQL Editor: + +```bash +# File: database-schema-skills.sql +``` + +This will create: +- Skills table +- SkillEndorsements table +- Indexes for performance +- RLS policies for security + +### 2. Verify Tables + +In Supabase Dashboard: +1. Go to Table Editor +2. Verify `Skills` and `SkillEndorsements` tables exist +3. Check that RLS is enabled on both tables + +### 3. Test the Feature + +1. Navigate to your profile at `/profile` +2. Click "Add Skill" to add a new skill +3. Visit another user's profile to endorse their skills +4. Click the thumbs-up icon to endorse/un-endorse + +## Component Architecture + +### SkillsSection Component + +Main component for displaying and managing skills. + +**Props:** +- `userId: string` - The profile owner's user ID +- `currentUserId?: string` - The logged-in user's ID +- `isOwnProfile: boolean` - Whether viewing own profile + +**Features:** +- Add new skills (own profile only) +- Delete skills (own profile only) +- Endorse/un-endorse skills (other profiles only) +- Real-time endorsement counts + +### Custom Hooks + +#### useUserSkills(userId, currentUserId) +Fetches skills for a user with endorsement data. + +**Returns:** +- Array of skills with endorsement counts +- Whether current user has endorsed each skill + +#### useAddSkill() +Mutation hook to add a new skill. + +#### useDeleteSkill() +Mutation hook to remove a skill. + +#### useEndorseSkill() +Mutation hook to endorse a skill. + +#### useRemoveEndorsement() +Mutation hook to remove an endorsement. + +## Usage Examples + +### Adding Skills to Profile + +```typescript +const { mutateAsync: addSkill } = useAddSkill(); + +await addSkill({ + userId: user.id, + skillName: 'React' +}); +``` + +### Endorsing a Skill + +```typescript +const { mutateAsync: endorseSkill } = useEndorseSkill(); + +await endorseSkill({ + skillId: 123, + endorserId: currentUser.id, + profileUserId: profileOwner.id +}); +``` + +### Fetching Skills + +```typescript +const { data: skills } = useUserSkills(userId, currentUserId); + +skills?.map(skill => ( +
+ {skill.skill_name} - {skill.endorsement_count} endorsements +
+)); +``` + +## API Endpoints + +All operations use Supabase client with the following patterns: + +### Get User Skills +```typescript +supabase + .from('Skills') + .select('*') + .eq('user_id', userId) +``` + +### Add Skill +```typescript +supabase + .from('Skills') + .insert({ user_id: userId, skill_name: skillName }) +``` + +### Delete Skill +```typescript +supabase + .from('Skills') + .delete() + .eq('id', skillId) +``` + +### Endorse Skill +```typescript +supabase + .from('SkillEndorsements') + .insert({ skill_id: skillId, endorser_id: endorserId }) +``` + +### Remove Endorsement +```typescript +supabase + .from('SkillEndorsements') + .delete() + .eq('skill_id', skillId) + .eq('endorser_id', endorserId) +``` + +## TypeScript Types + +```typescript +interface Skill { + id: number; + user_id: string; + skill_name: string; + created_at: string; +} + +interface SkillEndorsement { + id: number; + skill_id: number; + endorser_id: string; + created_at: string; +} + +interface SkillWithEndorsements extends Skill { + endorsement_count: number; + user_has_endorsed: boolean; +} +``` + +## Styling + +The component uses Tailwind CSS with dark mode support: +- Skills displayed as rounded pills +- Blue highlight for endorsed skills +- Hover effects for interactive elements +- Responsive design + +## Error Handling + +The system handles common errors: +- Duplicate skill names (prevented by unique constraint) +- Self-endorsement attempts (prevented by RLS policy) +- Multiple endorsements (prevented by unique constraint) +- Unauthorized operations (handled by RLS) + +## Performance Optimizations + +- Indexed queries on user_id and skill_id +- Optimistic updates with TanStack Query +- Efficient cache invalidation +- Batch loading of endorsement counts + +## Future Enhancements + +Potential improvements: +- Skill categories/tags +- Trending skills +- Skill recommendations +- Endorsement notifications +- Skill verification badges +- Export skills to resume + +## Troubleshooting + +### Skills not appearing +- Check RLS policies are enabled +- Verify user is authenticated +- Check browser console for errors + +### Cannot endorse skills +- Ensure you're not on your own profile +- Verify you're logged in +- Check database permissions + +### Duplicate skill error +- Each user can only have one instance of each skill +- Try a different skill name or variation + +## Contributing + +When contributing to this feature: +1. Follow existing code patterns +2. Add TypeScript types for new data structures +3. Update documentation +4. Test with multiple users +5. Verify RLS policies work correctly + +## License + +This feature is part of DevConnect and follows the project's MIT License. diff --git a/database-schema-skills.sql b/database-schema-skills.sql new file mode 100644 index 00000000..60f31e9c --- /dev/null +++ b/database-schema-skills.sql @@ -0,0 +1,39 @@ +-- Skills Table +CREATE TABLE Skills ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + skill_name TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(user_id, skill_name) +); + +-- Skill Endorsements Table +CREATE TABLE SkillEndorsements ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + skill_id BIGINT NOT NULL REFERENCES Skills(id) ON DELETE CASCADE, + endorser_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(skill_id, endorser_id) +); + +-- Indexes for performance +CREATE INDEX idx_skills_user_id ON Skills(user_id); +CREATE INDEX idx_endorsements_skill_id ON SkillEndorsements(skill_id); +CREATE INDEX idx_endorsements_endorser_id ON SkillEndorsements(endorser_id); + +-- Row Level Security (RLS) Policies +ALTER TABLE Skills ENABLE ROW LEVEL SECURITY; +ALTER TABLE SkillEndorsements ENABLE ROW LEVEL SECURITY; + +-- Skills: Anyone can read, only owner can insert/delete +CREATE POLICY "Skills are viewable by everyone" ON Skills FOR SELECT USING (true); +CREATE POLICY "Users can add their own skills" ON Skills FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY "Users can delete their own skills" ON Skills FOR DELETE USING (auth.uid() = user_id); + +-- Endorsements: Anyone can read, users can endorse others' skills +CREATE POLICY "Endorsements are viewable by everyone" ON SkillEndorsements FOR SELECT USING (true); +CREATE POLICY "Users can endorse skills" ON SkillEndorsements FOR INSERT WITH CHECK ( + auth.uid() = endorser_id AND + auth.uid() != (SELECT user_id FROM Skills WHERE id = skill_id) +); +CREATE POLICY "Users can remove their endorsements" ON SkillEndorsements FOR DELETE USING (auth.uid() = endorser_id); diff --git a/src/components/SkillsSection.tsx b/src/components/SkillsSection.tsx new file mode 100644 index 00000000..0d8bc075 --- /dev/null +++ b/src/components/SkillsSection.tsx @@ -0,0 +1,150 @@ +import { useState } from 'react'; +import { Plus, X, ThumbsUp } from 'lucide-react'; +import { useUserSkills, useAddSkill, useDeleteSkill, useEndorseSkill, useRemoveEndorsement } from '../hooks/useSkills'; + +interface SkillsSectionProps { + userId: string; + currentUserId?: string; + isOwnProfile: boolean; +} + +export default function SkillsSection({ userId, currentUserId, isOwnProfile }: SkillsSectionProps) { + const [newSkill, setNewSkill] = useState(''); + const [isAdding, setIsAdding] = useState(false); + + const { data: skills, isLoading } = useUserSkills(userId, currentUserId); + const addSkillMutation = useAddSkill(); + const deleteSkillMutation = useDeleteSkill(); + const endorseSkillMutation = useEndorseSkill(); + const removeEndorsementMutation = useRemoveEndorsement(); + + const handleAddSkill = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newSkill.trim()) return; + + try { + await addSkillMutation.mutateAsync({ userId, skillName: newSkill.trim() }); + setNewSkill(''); + setIsAdding(false); + } catch (error: any) { + alert(error.message || 'Failed to add skill'); + } + }; + + const handleDeleteSkill = async (skillId: number) => { + if (!confirm('Remove this skill?')) return; + try { + await deleteSkillMutation.mutateAsync({ skillId, userId }); + } catch (error: any) { + alert(error.message || 'Failed to delete skill'); + } + }; + + const handleEndorse = async (skillId: number) => { + if (!currentUserId) { + alert('Please sign in to endorse skills'); + return; + } + + const skill = skills?.find(s => s.id === skillId); + if (!skill) return; + + try { + if (skill.user_has_endorsed) { + await removeEndorsementMutation.mutateAsync({ skillId, endorserId: currentUserId, profileUserId: userId }); + } else { + await endorseSkillMutation.mutateAsync({ skillId, endorserId: currentUserId, profileUserId: userId }); + } + } catch (error: any) { + alert(error.message || 'Failed to update endorsement'); + } + }; + + if (isLoading) { + return
Loading skills...
; + } + + return ( +
+
+

Skills

+ {isOwnProfile && !isAdding && ( + + )} +
+ + {isAdding && ( +
+ setNewSkill(e.target.value)} + placeholder="Enter skill name" + className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" + autoFocus + /> + + +
+ )} + + {!skills || skills.length === 0 ? ( +

+ {isOwnProfile ? 'Add your first skill to get started!' : 'No skills added yet.'} +

+ ) : ( +
+ {skills.map((skill) => ( +
+ + {skill.skill_name} + + + {isOwnProfile && ( + + )} +
+ ))} +
+ )} +
+ ); +} diff --git a/src/hooks/useSkills.ts b/src/hooks/useSkills.ts new file mode 100644 index 00000000..09d42e62 --- /dev/null +++ b/src/hooks/useSkills.ts @@ -0,0 +1,120 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { supabase } from '../supabase-client'; +import { SkillWithEndorsements } from '../types/skills'; + +export const useUserSkills = (userId: string, currentUserId?: string) => { + return useQuery({ + queryKey: ['skills', userId, currentUserId], + queryFn: async () => { + const { data: skills, error } = await supabase + .from('Skills') + .select('*') + .eq('user_id', userId) + .order('created_at', { ascending: false }); + + if (error) throw error; + + const skillsWithEndorsements: SkillWithEndorsements[] = await Promise.all( + (skills || []).map(async (skill) => { + const { count } = await supabase + .from('SkillEndorsements') + .select('*', { count: 'exact', head: true }) + .eq('skill_id', skill.id); + + let userHasEndorsed = false; + if (currentUserId) { + const { data } = await supabase + .from('SkillEndorsements') + .select('id') + .eq('skill_id', skill.id) + .eq('endorser_id', currentUserId) + .single(); + userHasEndorsed = !!data; + } + + return { + ...skill, + endorsement_count: count || 0, + user_has_endorsed: userHasEndorsed, + }; + }) + ); + + return skillsWithEndorsements; + }, + enabled: !!userId, + }); +}; + +export const useAddSkill = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ userId, skillName }: { userId: string; skillName: string }) => { + const { data, error } = await supabase + .from('Skills') + .insert({ user_id: userId, skill_name: skillName }) + .select() + .single(); + + if (error) throw error; + return data; + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['skills', variables.userId] }); + }, + }); +}; + +export const useDeleteSkill = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ skillId, userId }: { skillId: number; userId: string }) => { + const { error } = await supabase.from('Skills').delete().eq('id', skillId); + if (error) throw error; + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['skills', variables.userId] }); + }, + }); +}; + +export const useEndorseSkill = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ skillId, endorserId, profileUserId }: { skillId: number; endorserId: string; profileUserId: string }) => { + const { data, error } = await supabase + .from('SkillEndorsements') + .insert({ skill_id: skillId, endorser_id: endorserId }) + .select() + .single(); + + if (error) throw error; + return data; + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['skills', variables.profileUserId] }); + }, + }); +}; + +export const useRemoveEndorsement = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ skillId, endorserId, profileUserId }: { skillId: number; endorserId: string; profileUserId: string }) => { + const { error } = await supabase + .from('SkillEndorsements') + .delete() + .eq('skill_id', skillId) + .eq('endorser_id', endorserId); + + if (error) throw error; + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['skills', variables.profileUserId] }); + }, + }); +}; diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index cc442707..16e13c78 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -2,6 +2,7 @@ import { useAuth } from '../hooks/useAuth'; import { useNavigate } from "react-router"; import { User, Mail, Calendar, Shield } from "lucide-react"; import { format } from "date-fns"; +import SkillsSection from '../components/SkillsSection'; export default function ProfilePage() { const { user, signOut } = useAuth(); @@ -78,6 +79,10 @@ export default function ProfilePage() { +
+ +
+