diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..5e49e1a Binary files /dev/null and b/backend/.gitignore differ diff --git a/backend/env.example b/backend/env.example index 26d4331..ade1d26 100644 --- a/backend/env.example +++ b/backend/env.example @@ -4,6 +4,9 @@ JWT_SECRET= GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_CALLBACK_URL= +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_CALLBACK_URL= CLIENT_URL= SESSION_SECRET= ADMIN_EMAIL= diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index db40e01..f4b041e 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -5,12 +5,18 @@ require('dotenv').config(); const JWT_SECRET = process.env.JWT_SECRET || 'devsync_secure_jwt_secret_key_for_authentication'; module.exports = function(req, res, next) { - // Get token from header + // Check if user is authenticated via Passport session (GitHub, Google) + if (req.isAuthenticated && req.isAuthenticated()) { + console.log('User authenticated via session:', req.user); + return next(); + } + + // Get token from header for JWT auth const token = req.header('x-auth-token'); // Check if no token if (!token) { - return res.status(401).json({ errors: [{ msg: 'No token, authorization denied' }] }); + return res.status(401).json({ errors: [{ msg: 'No authentication, authorization denied' }] }); } // Verify token diff --git a/backend/middleware/rateLimit/authLimiterMiddleware.js b/backend/middleware/rateLimit/authLimiterMiddleware.js index 59a22fb..db01791 100644 --- a/backend/middleware/rateLimit/authLimiterMiddleware.js +++ b/backend/middleware/rateLimit/authLimiterMiddleware.js @@ -1,7 +1,7 @@ const { RateLimiterMemory } = require('rate-limiter-flexible'); exports.authLimiter = new RateLimiterMemory({ - points: 5, - duration: 60, - blockDuration: 60 * 5, + points: 20, // Increased from 5 to 20 attempts + duration: 60, // Per minute + blockDuration: 60 * 2, // Reduced block time to 2 minutes }) \ No newline at end of file diff --git a/backend/models/Feedback.js b/backend/models/Feedback.js new file mode 100644 index 0000000..0faaa6b --- /dev/null +++ b/backend/models/Feedback.js @@ -0,0 +1,35 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const FeedbackSchema = new Schema({ + userId: { + type: String, + required: true + }, + rating: { + type: Number, + required: true, + min: 1, + max: 5 + }, + comment: { + type: String, + required: true, + minlength: 10 + }, + category: { + type: String, + default: 'other', + enum: ['ui', 'features', 'bugs', 'suggestions', 'other'] + }, + isAnonymous: { + type: Boolean, + default: false + }, + date: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('Feedback', FeedbackSchema); \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js index 73d8fee..1f4d540 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -12,6 +12,11 @@ const UserSchema = new Schema({ unique: true, sparse: true, // multiple nulls allowed }, + githubId: { + type: String, + unique: true, + sparse: true, // multiple nulls allowed + }, name: { type: String, required: true, @@ -29,7 +34,7 @@ const UserSchema = new Schema({ password: { type: String, required: function () { - return !this.googleId; + return !this.googleId && !this.githubId; }, }, avatar: { diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 88cecae..d3b5d0f 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -440,10 +440,41 @@ router.get("/", auth, async (req, res) => { // @access Private router.get("/me", (req, res) => { if (req.isAuthenticated()) { + console.log("User is authenticated via session:", req.user); res.json(req.user); } else { res.status(401).json({ message: "Not logged in" }); } }); +// @route GET api/auth/check +// @desc Check if user is authenticated (works for both JWT and session auth) +// @access Public +router.get("/check", (req, res) => { + if (req.isAuthenticated()) { + return res.json({ + isAuthenticated: true, + authMethod: 'session', + user: req.user + }); + } + + const token = req.header('x-auth-token'); + if (token) { + try { + const JWT_SECRET = process.env.JWT_SECRET || 'devsync_secure_jwt_secret_key_for_authentication'; + const decoded = jwt.verify(token, JWT_SECRET); + return res.json({ + isAuthenticated: true, + authMethod: 'token', + user: decoded.user + }); + } catch (err) { + console.error('Token verification error:', err.message); + } + } + + res.json({ isAuthenticated: false }); +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/feedback.js b/backend/routes/feedback.js new file mode 100644 index 0000000..00f3121 --- /dev/null +++ b/backend/routes/feedback.js @@ -0,0 +1,116 @@ +const express = require('express'); +const router = express.Router(); +const auth = require('../middleware/auth'); +const Feedback = require('../models/Feedback'); + +// @route POST api/feedback +// @desc Submit user feedback +// @access Private +router.post('/', auth, async (req, res) => { + try { + const { rating, comment, category, isAnonymous } = req.body; + + // Validate the data + if (!rating || rating < 1 || rating > 5) { + return res.status(400).json({ message: 'Please provide a valid rating between 1 and 5' }); + } + + if (!comment || comment.trim().length < 10) { + return res.status(400).json({ message: 'Please provide feedback with at least 10 characters' }); + } + + // Create a new feedback instance + const newFeedback = new Feedback({ + userId: req.user.id, + rating, + comment, + category: category || 'other', + isAnonymous: isAnonymous || false + }); + + // Save the feedback to the database + await newFeedback.save(); + + res.json({ success: true, message: 'Feedback submitted successfully' }); + } catch (err) { + console.error('Error submitting feedback:', err.message); + res.status(500).json({ message: 'Server error' }); + } +}); + +// @route POST api/feedback/guest +// @desc Submit guest (unauthenticated) feedback +// @access Public +router.post('/guest', async (req, res) => { + try { + const { rating, comment, category, isAnonymous = true } = req.body; + + // Validate the data + if (!rating || rating < 1 || rating > 5) { + return res.status(400).json({ message: 'Please provide a valid rating between 1 and 5' }); + } + + if (!comment || comment.trim().length < 10) { + return res.status(400).json({ message: 'Please provide feedback with at least 10 characters' }); + } + + // Create a new feedback instance for guest user + const newFeedback = new Feedback({ + userId: "guest", + rating, + comment, + category: category || 'other', + isAnonymous: true // Always anonymous for guests + }); + + // Save the feedback to the database + await newFeedback.save(); + + res.json({ success: true, message: 'Guest feedback submitted successfully' }); + } catch (err) { + console.error('Error submitting guest feedback:', err.message); + res.status(500).json({ message: 'Server error' }); + } +}); + +// @route GET api/feedback +// @desc Get all feedback (for admin/community page) +// @access Public +router.get('/', async (req, res) => { + try { + console.log("Feedback GET request received"); + + // Get feedback sorted by date (newest first) + try { + const feedbackList = await Feedback.find() + .sort({ date: -1 }) + .select( + // Don't include user ID if feedback is anonymous + '-__v ' + (req.query.includePrivate === 'true' ? '' : '-userId') + ); + + // Process feedback for public display + const processedFeedback = feedbackList.map(feedback => { + const feedbackObj = feedback.toObject(); + + // If feedback is anonymous, remove any identifiable information + if (feedbackObj.isAnonymous && !req.query.includePrivate) { + feedbackObj.userId = 'anonymous'; + } + + return feedbackObj; + }); + + return res.json(processedFeedback); + } catch (dbError) { + console.error("Database error:", dbError); + // If database operation fails, return empty array + return res.json([]); + } + } catch (err) { + console.error('Error getting feedback:', err.message); + res.status(500).json({ message: 'Server error' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/github.js b/backend/routes/github.js new file mode 100644 index 0000000..86c03c4 --- /dev/null +++ b/backend/routes/github.js @@ -0,0 +1,176 @@ +const express = require('express'); +const router = express.Router(); +const auth = require('../middleware/auth'); +const { getGitHubActivity, calculateStreak } = require('../utils/githubSync'); +const { generateGitHubData } = require('../utils/sampleData'); + +// @route GET api/github/sync +// @desc Sync GitHub data for current user +// @access Private +router.get('/sync', auth, async (req, res) => { + try { + console.log("GitHub sync route hit"); + console.log("User data available:", !!req.user); + + // Print user properties for debugging but hide sensitive values + if (req.user) { + const userProps = Object.keys(req.user).map(key => { + if (key === 'accessToken') { + return `${key}: ${req.user[key] ? '[TOKEN AVAILABLE]' : '[TOKEN MISSING]'}`; + } + return key; + }); + console.log("User properties:", userProps); + } + + // Only works for GitHub authenticated users + if (!req.isAuthenticated || !req.isAuthenticated() || !req.user.githubId) { + return res.status(400).json({ message: 'This endpoint only works for GitHub authenticated users' }); + } + + // Get GitHub username from user profile + const username = req.user.username || req.user.name; + + // Try to get access token from various locations it might be stored + const accessToken = req.user.accessToken || + (req.user._json && req.user._json.accessToken) || + (req.user.profile && req.user.profile.accessToken) || + req.session.accessToken; + + console.log("Using username:", username); + console.log("Access token available:", !!accessToken); + + if (!username) { + return res.status(400).json({ message: 'Missing GitHub username' }); + } + + if (!accessToken) { + console.log("Access token missing. Available user data:", + Object.keys(req.user).filter(k => k !== 'accessToken').join(', ')); + return res.status(401).json({ message: 'Missing GitHub access token - please log out and log in again' }); + } + + // Check if we should use sample data (for development/testing) + const useSampleData = process.env.USE_SAMPLE_GITHUB_DATA === 'true' || false; + let activityData; + + if (useSampleData) { + console.log("Using sample GitHub data for development"); + activityData = generateGitHubData(username); + } else { + // Get GitHub activity and profile data from real API + activityData = await getGitHubActivity(username, accessToken); + console.log("Received activity data from getGitHubActivity:", + activityData ? "success" : "failed"); + + // Check if we got a valid result object + if (!activityData || typeof activityData !== 'object' || + !activityData.activities || activityData.activities.length === 0) { + console.log("No activity data returned, falling back to sample data"); + activityData = generateGitHubData(username); + } + } + + // Extract the activities array from the result object + const activities = activityData.activities || []; + console.log(`Found ${activities.length} activities in the data`); + + // Calculate streak - limit to a reasonable value if using sample data + let streak = calculateStreak(activities); + if (useSampleData || activityData === generateGitHubData(username)) { + // If using sample data, limit the streak to a more reasonable value + streak = Math.min(streak, 7); // Cap sample data streak at 7 days + console.log(`Using sample data - capping streak at 7 days`); + } + console.log(`Calculated streak: ${streak}`); + + // Extract GitHub profile and repos data + const githubProfile = activityData.githubProfile || {}; + const repositories = activityData.repositories || []; + console.log(`Found ${repositories.length} repositories`); + + // Add some sample activity data for testing if no real data is available + let processedActivity = []; + + if (activities.length > 0) { + console.log("Processing activity data for heatmap visualization"); + // Process activity data for heatmap visualization + processedActivity = activities + .filter(item => typeof item === 'object' && item !== null) + .map(item => { + // Ensure each item has a date field in YYYY-MM-DD format + if (!item.date && item.created_at) { + item.date = new Date(item.created_at).toISOString().split('T')[0]; + } + + // Format for calendar heatmap + return { + ...item, + day: item.date || item.day, + value: item.value || 1 // Base contribution value + }; + }); + + console.log(`Processed ${processedActivity.length} activities for the heatmap`); + } + + // If no activity data, create sample data for development/testing + if (processedActivity.length === 0) { + console.log("No GitHub activity found, creating sample data for testing"); + + // Generate sample data for the last 30 days + const today = new Date(); + for (let i = 0; i < 30; i++) { + const date = new Date(); + date.setDate(today.getDate() - i); + const dateStr = date.toISOString().split('T')[0]; + + // Random activity value between 0-5 + const value = Math.floor(Math.random() * 6); + + if (value > 0) { // Only add days with activity + processedActivity.push({ + date: dateStr, + day: dateStr, + value: value, + type: "SampleActivity", + details: "Sample activity for testing" + }); + } + } + } + + // Create a complete GitHub data object + const githubData = { + activity: processedActivity, + profile: githubProfile, + repositories: repositories, + streak: streak + }; + + // Update user session data + req.user.activity = processedActivity; + req.user.githubProfile = githubProfile; + req.user.repositories = repositories; + req.user.streak = streak; + + // Save updated session + req.session.save((err) => { + if (err) { + console.error('Error saving session:', err); + } + + // Return updated user data + res.json({ + success: true, + user: githubData + }); + }); + + } catch (err) { + console.error('GitHub sync error:', err.message); + res.status(500).json({ message: 'Server error during GitHub sync' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/profile.js b/backend/routes/profile.js index 07ff0db..ad94c52 100644 --- a/backend/routes/profile.js +++ b/backend/routes/profile.js @@ -9,6 +9,58 @@ const fs = require('fs'); const crypto = require('crypto'); const LeetCode = require("../models/Leetcode") +// @route GET api/profile +// @desc Get user profile +// @access Private +router.get('/', auth, async (req, res) => { + try { + // For GitHub/Google authenticated users (session-based) + if (req.isAuthenticated && req.isAuthenticated()) { + console.log('Profile route: User authenticated via session:', req.user); + + // Return the complete user object with all fields + // This uses the enhanced user object we created during authentication + if (req.user.platforms && req.user.streak !== undefined) { + return res.json(req.user); + } + + // Fallback if we don't have the enhanced user object + return res.json({ + name: req.user.name || req.user.displayName || 'Social Auth User', + email: req.user.email || '', + avatar: req.user.avatar || req.user.photos?.[0]?.value || generateAvatarUrl(req.user.email, req.user.name), + platforms: req.user.username ? [ + { + name: 'GitHub', + username: req.user.username, + url: `https://github.com/${req.user.username}` + } + ] : [], + streak: 0, + timeSpent: '0 minutes', + notes: [], + activity: [], + goals: [] + }); + } + + // For regular JWT users with MongoDB records + if (req.user && req.user.id) { + let user = await User.findById(req.user.id).select('-password'); + if (!user) { + return res.status(404).json({ msg: 'User not found' }); + } + return res.json(user); + } + + // If we get here, something's wrong with authentication + return res.status(401).json({ errors: [{ msg: 'Authentication failed' }] }); + } catch (err) { + console.error('Profile error:', err.message); + res.status(500).json({ errors: [{ msg: 'Server Error' }] }); + } +}); + // Helper function to generate avatar URL from email or name const generateAvatarUrl = (email, name) => { // Use email for consistent avatar, or fallback to name diff --git a/backend/server.js b/backend/server.js index 749be65..2c20dd3 100644 --- a/backend/server.js +++ b/backend/server.js @@ -64,6 +64,7 @@ app.use("/api/auth", authMiddleware, require("./routes/auth")); app.use("/api/profile", generalMiddleware, require("./routes/profile")); app.use("/api/contact", generalMiddleware, contactRouter); app.use("/api/tasks", require("./routes/tasks.route")); +app.use("/api/feedback", require("./routes/feedback")); // Default route diff --git a/backend/utils/githubSync.js b/backend/utils/githubSync.js new file mode 100644 index 0000000..fbdba0e --- /dev/null +++ b/backend/utils/githubSync.js @@ -0,0 +1,468 @@ +/** + * GitHub synchronization utilities to pull user data from GitHub API + */ +const fetch = require('node-fetch'); + +/** + * Fetch GitHub user activity data and profile information + * @param {string} username - GitHub username + * @param {string} accessToken - GitHub OAuth access token + * @returns {Promise} - Object containing activity data and user profile + */ +async function getGitHubActivity(username, accessToken) { + try { + console.log(`Fetching GitHub activity for user: ${username}`); + console.log(`Access token available: ${!!accessToken}`); + + // Build headers + const headers = { + 'Accept': 'application/vnd.github.v3+json' + }; + + // Only add Authorization header if we have a token + if (accessToken) { + // GitHub API accepts both "token" and "Bearer" formats, but Bearer is more standard for OAuth 2.0 + headers['Authorization'] = `Bearer ${accessToken}`; + console.log("Using Bearer token for GitHub API requests"); + } else { + console.log("No token available for GitHub API requests"); + } + + // Fetch user profile from GitHub + const profileData = await fetchGitHubProfile(username, accessToken); + console.log("Fetched GitHub profile:", profileData ? "success" : "failed"); + + // GitHub Events API - Get user events with detailed debug logging + console.log(`Fetching events for user: ${username}`); + console.log(`API URL: https://api.github.com/users/${username}/events`); + console.log(`Headers: ${JSON.stringify(headers, (key, value) => + key === 'Authorization' ? 'Bearer [TOKEN HIDDEN]' : value)}`); + + const response = await fetch(`https://api.github.com/users/${username}/events`, { + headers: headers + }); + + if (!response.ok) { + console.error(`GitHub Events API error: ${response.status} ${response.statusText}`); + throw new Error(`GitHub API error: ${response.status}`); + } + + const events = await response.json(); + console.log(`Fetched ${events.length} events from GitHub`); + + // Also get user's repositories + const repositories = await fetchUserRepositories(username, accessToken); + console.log(`Fetched ${repositories.length} repositories from GitHub`); + + // Also get contributions data using the GraphQL API for better heatmap visualization + console.log("Attempting to fetch contribution data..."); + let contributionData = []; + if (accessToken) { + contributionData = await fetchContributionCalendar(accessToken); + console.log(`Fetched ${contributionData.length} contribution data points`); + } else { + console.log("Skipping contribution calendar fetch - no token available"); + } + + // Transform GitHub events into activity entries + console.log("Processing GitHub events into activity entries"); + const eventActivities = events.slice(0, 30).map(event => { + const date = new Date(event.created_at); + return { + date: date.toISOString().split('T')[0], // YYYY-MM-DD + day: date.toISOString().split('T')[0], // Format for heatmap + value: 1, // Base value for heatmap + type: event.type, + repo: event.repo?.name || "unknown", + details: getEventDetails(event) + }; + }); + + console.log(`Processed ${eventActivities.length} event activities`); + + // Combine event activities with contribution data + const allActivities = [...eventActivities, ...contributionData]; + console.log(`Total activities to process: ${allActivities.length}`); + + // Group by date to prevent duplicates and limit to most recent 60 days + const activityMap = new Map(); + + // Get cutoff date (60 days ago) + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - 60); + const cutoffDateStr = cutoffDate.toISOString().split('T')[0]; + + console.log(`Filtering activities to only show from ${cutoffDateStr} onwards`); + + allActivities.forEach(activity => { + const day = activity.date || activity.day; + if (!day) return; + + // Only include activities from the last 60 days + if (day >= cutoffDateStr) { + if (activityMap.has(day)) { + // Increment count for existing day + const existingActivity = activityMap.get(day); + existingActivity.value = (existingActivity.value || 0) + (activity.value || 1); + } else { + // Create new entry + activityMap.set(day, { + ...activity, + day: day, + date: day, + value: activity.value || 1 + }); + } + } + }); + + console.log(`Grouped activities by date: ${activityMap.size} unique days (after filtering)`); + + // Create the result array + const result = Array.from(activityMap.values()); + + // Create a proper return object instead of attaching properties to the array + // This will fix the "activity is not iterable" issue + return { + activities: result, + githubProfile: profileData, + repositories: repositories + }; + + return activityData; + } catch (error) { + console.error('Error fetching GitHub activity:', error); + return []; + } +} + +/** + * Fetch GitHub user profile information + * @param {string} username - GitHub username + * @param {string} accessToken - GitHub OAuth access token + * @returns {Promise} - GitHub user profile data + */ +async function fetchGitHubProfile(username, accessToken) { + try { + const headers = { + 'Accept': 'application/vnd.github.v3+json' + }; + + if (accessToken) { + headers['Authorization'] = `Bearer ${accessToken}`; + console.log(`Adding Bearer token to GitHub profile request for ${username}`); + } + + const response = await fetch(`https://api.github.com/users/${username}`, { + headers: headers + }); + + if (!response.ok) { + console.error(`Error fetching GitHub profile: ${response.status}`); + return {}; + } + + return await response.json(); + } catch (error) { + console.error('Error fetching GitHub profile:', error); + return {}; + } +} + +/** + * Fetch user's GitHub repositories + * @param {string} username - GitHub username + * @param {string} accessToken - GitHub OAuth access token + * @returns {Promise} - Array of repository data + */ +async function fetchUserRepositories(username, accessToken) { + try { + const headers = { + 'Accept': 'application/vnd.github.v3+json' + }; + + if (accessToken) { + headers['Authorization'] = `Bearer ${accessToken}`; + console.log(`Adding Bearer token to repository request for ${username}`); + } + + const response = await fetch(`https://api.github.com/users/${username}/repos?sort=updated&per_page=10`, { + headers: headers + }); + + if (!response.ok) { + console.error(`Error fetching GitHub repositories: ${response.status}`); + return []; + } + + const repos = await response.json(); + + // Transform repo data to include only what we need + return repos.map(repo => ({ + id: repo.id, + name: repo.name, + full_name: repo.full_name, + description: repo.description, + html_url: repo.html_url, + language: repo.language, + stargazers_count: repo.stargazers_count, + forks_count: repo.forks_count, + updated_at: repo.updated_at + })); + } catch (error) { + console.error('Error fetching GitHub repositories:', error); + return []; + } +} + +/** + * Fetch contribution calendar data using GitHub GraphQL API + * @param {string} accessToken - GitHub OAuth access token + * @returns {Promise} - Array of contribution data + */ +async function fetchContributionCalendar(accessToken) { + try { + console.log("Attempting to fetch contribution calendar via GraphQL API"); + // GraphQL query to fetch contribution calendar + const query = ` + query { + viewer { + contributionsCollection { + contributionCalendar { + weeks { + contributionDays { + date + contributionCount + } + } + } + } + } + } + `; + + console.log("GraphQL Query:", query); + console.log("GraphQL API URL: https://api.github.com/graphql"); + console.log("Using Authorization header with Bearer token"); + + const response = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}` + }, + body: JSON.stringify({ query }) + }); + + if (!response.ok) { + console.warn(`GitHub GraphQL API error: ${response.status}`); + // If GraphQL API fails, fall back to the REST API for commit stats + return await fetchCommitActivity(accessToken); + } + + const result = await response.json(); + + // Check for GraphQL errors + if (result.errors) { + console.warn('GitHub GraphQL API returned errors:', result.errors); + // Fall back to REST API + return await fetchCommitActivity(accessToken); + } + + // Extract contribution data + const calendar = result.data?.viewer?.contributionsCollection?.contributionCalendar; + if (!calendar) { + console.warn('No calendar data in GraphQL response'); + return await fetchCommitActivity(accessToken); + } + + // Flatten the weeks array and map to the format we need + const contributionDays = []; + calendar.weeks.forEach(week => { + week.contributionDays.forEach(day => { + contributionDays.push({ + day: day.date, // YYYY-MM-DD + date: day.date, + value: day.contributionCount + }); + }); + }); + + return contributionDays; + } catch (error) { + console.error('Error fetching contribution calendar:', error); + // Fall back to REST API + return await fetchCommitActivity(accessToken); + } +} + +/** + * Fallback function to fetch commit activity using REST API + * @param {string} accessToken - GitHub OAuth access token + * @returns {Promise} - Array of activity data + */ +async function fetchCommitActivity(accessToken) { + try { + // Get authenticated user to get username + const headers = { + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': `Bearer ${accessToken}` + }; + + const userResponse = await fetch('https://api.github.com/user', { + headers + }); + + if (!userResponse.ok) { + console.error('Error fetching user data:', userResponse.status); + return []; + } + + const userData = await userResponse.json(); + const username = userData.login; + + // Get user's repositories + const reposResponse = await fetch(`https://api.github.com/users/${username}/repos?per_page=100`, { + headers + }); + + if (!reposResponse.ok) { + console.error('Error fetching repositories:', reposResponse.status); + return []; + } + + const repos = await reposResponse.json(); + + // Create a map to store commit counts by date + const commitsByDate = {}; + + // For each repo, get commit activity + const commitPromises = repos.slice(0, 10).map(async (repo) => { + try { + const commitsResponse = await fetch( + `https://api.github.com/repos/${repo.full_name}/commits?author=${username}&per_page=100`, + { headers } + ); + + if (!commitsResponse.ok) return []; + + const commits = await commitsResponse.json(); + + // Process commits + commits.forEach(commit => { + const date = new Date(commit.commit.committer.date) + .toISOString().split('T')[0]; + + if (!commitsByDate[date]) { + commitsByDate[date] = 0; + } + + commitsByDate[date]++; + }); + } catch (error) { + console.error(`Error fetching commits for ${repo.full_name}:`, error); + } + }); + + // Wait for all commit fetching to complete + await Promise.all(commitPromises); + + // Convert to array format + return Object.entries(commitsByDate).map(([date, count]) => ({ + day: date, + date, + value: count + })); + } catch (error) { + console.error('Error in fetchCommitActivity:', error); + return []; + } +} + +/** + * Extract meaningful details from different GitHub event types + */ +function getEventDetails(event) { + switch (event.type) { + case 'PushEvent': + return `Pushed ${event.payload.commits?.length || 0} commit(s)`; + case 'PullRequestEvent': + return `${event.payload.action} pull request #${event.payload.number}`; + case 'IssuesEvent': + return `${event.payload.action} issue #${event.payload.issue?.number}`; + case 'CreateEvent': + return `Created ${event.payload.ref_type} ${event.payload.ref || ''}`; + case 'DeleteEvent': + return `Deleted ${event.payload.ref_type} ${event.payload.ref || ''}`; + case 'WatchEvent': + return 'Starred a repository'; + case 'ForkEvent': + return 'Forked a repository'; + default: + return event.type; + } +} + +/** + * Calculate GitHub streak from activity data + * @param {Array} activity - GitHub activity data + * @returns {number} - Current streak count + */ +function calculateStreak(activity) { + if (!activity || activity.length === 0) return 0; + + console.log(`Calculating streak from ${activity.length} activities`); + + // Sort activity by date + const sortedActivity = [...activity].sort((a, b) => { + const dateA = a.date || a.day; + const dateB = b.date || b.day; + return new Date(dateB) - new Date(dateA); + }); + + // Group by date (to count only one contribution per day) + const uniqueDays = new Set(); + sortedActivity.forEach(item => { + const date = item.date || item.day; + if (date) uniqueDays.add(date); + }); + const days = Array.from(uniqueDays); + console.log(`Found ${days.length} unique days with activity`); + + // Calculate streak + let currentStreak = 0; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const todayStr = today.toISOString().split('T')[0]; + + // Check if there's activity today + const hasTodayActivity = days.includes(todayStr); + + // If no activity today, start checking from yesterday + let currentDate = new Date(today); + if (!hasTodayActivity) { + currentDate.setDate(currentDate.getDate() - 1); + } + + while (true) { + const dateStr = currentDate.toISOString().split('T')[0]; + if (days.includes(dateStr)) { + currentStreak++; + } else { + break; + } + + // Move to the previous day + currentDate.setDate(currentDate.getDate() - 1); + } + + return currentStreak; +} + +module.exports = { + getGitHubActivity, + calculateStreak, + fetchGitHubProfile, + fetchUserRepositories +}; \ No newline at end of file diff --git a/backend/utils/sampleData.js b/backend/utils/sampleData.js new file mode 100644 index 0000000..2dcf0ae --- /dev/null +++ b/backend/utils/sampleData.js @@ -0,0 +1,140 @@ +/** + * Utility to create sample GitHub data for testing + */ + +/** + * Generate sample GitHub activity data for testing + * @param {number} days - Number of days to generate data for + * @returns {Array} - Array of sample activity data + */ +function generateSampleActivityData(days = 30) { + console.log(`Generating ${days} days of sample activity data`); + const today = new Date(); + const sampleData = []; + + // Pre-define specific days to have activity instead of random + // This creates a more realistic pattern with occasional activity + const activityDays = [0, 2, 5, 9, 12, 15, 19, 21, 25, 28]; // Days with activity + + for (let i = 0; i < days; i++) { + // Only create data for specific days + if (activityDays.includes(i)) { + const date = new Date(); + date.setDate(today.getDate() - i); + const dateStr = date.toISOString().split('T')[0]; + + // Generate 1-2 activities per day (random) + const activityCount = Math.floor(Math.random() * 2) + 1; + + sampleData.push({ + date: dateStr, + day: dateStr, + value: activityCount, + type: 'SampleCommitEvent', + repo: 'sample/repository', + details: `Sample commit activity (${activityCount} commits)` + }); + } + } + + return sampleData; +} + +/** + * Generate sample GitHub profile data + * @param {string} username - GitHub username + * @returns {Object} - Sample profile data + */ +function generateSampleProfile(username) { + return { + login: username, + id: 12345, + avatar_url: 'https://avatars.githubusercontent.com/u/583231?v=4', + html_url: `https://github.com/${username}`, + name: username, + company: 'Sample Company', + blog: 'https://example.com', + location: 'Sample Location', + email: null, + bio: 'Sample GitHub profile for testing purposes', + public_repos: 20, + public_gists: 5, + followers: 100, + following: 50, + created_at: '2011-01-25T18:44:36Z', + updated_at: '2023-09-15T12:30:45Z' + }; +} + +/** + * Generate sample GitHub repository data + * @param {string} username - GitHub username + * @returns {Array} - Array of sample repository data + */ +function generateSampleRepositories(username) { + const repoCount = 8; + const repos = []; + + const languages = ['JavaScript', 'TypeScript', 'Python', 'Java', 'HTML', 'CSS', 'Ruby', 'Go']; + const topics = ['web', 'api', 'react', 'node', 'frontend', 'backend', 'fullstack', 'mobile', 'data', 'ml']; + + for (let i = 1; i <= repoCount; i++) { + const language = languages[Math.floor(Math.random() * languages.length)]; + const starsCount = Math.floor(Math.random() * 1000); + const forksCount = Math.floor(Math.random() * 100); + + // Generate random date within the last year + const date = new Date(); + date.setDate(date.getDate() - Math.floor(Math.random() * 365)); + + // Get 2-4 random topics + const repoTopics = []; + const topicCount = Math.floor(Math.random() * 3) + 2; + for (let j = 0; j < topicCount; j++) { + const topic = topics[Math.floor(Math.random() * topics.length)]; + if (!repoTopics.includes(topic)) { + repoTopics.push(topic); + } + } + + repos.push({ + id: 100000 + i, + name: `repo-${i}`, + full_name: `${username}/repo-${i}`, + html_url: `https://github.com/${username}/repo-${i}`, + description: `Sample repository ${i} for testing purposes`, + language, + topics: repoTopics, + stargazers_count: starsCount, + forks_count: forksCount, + updated_at: date.toISOString(), + created_at: date.toISOString() + }); + } + + return repos; +} + +/** + * Generate complete GitHub data package for testing + * @param {string} username - GitHub username + * @returns {Object} - Complete GitHub data object + */ +function generateGitHubData(username) { + const activities = generateSampleActivityData(60); + const profile = generateSampleProfile(username); + const repositories = generateSampleRepositories(username); + + return { + activities, + githubProfile: profile, + repositories + }; +} + +module.exports = { + generateSampleActivityData, + generateSampleProfile, + generateSampleRepositories, + generateGitHubData +}; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4b64e87..e327597 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,11 +13,13 @@ "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.14", @@ -1740,6 +1742,66 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", @@ -2504,6 +2566,82 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -2780,6 +2918,21 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index e39eef1..2619f20 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,11 +15,13 @@ "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.14", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7aa514f..b14df54 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -28,6 +28,7 @@ import { ArrowUp } from "lucide-react"; import GitHubProfile from "./Components/GitHubProfile"; import LeetCode from "./Components/DashBoard/LeetCode"; import FloatingSupportButton from "./Components/ui/Support"; +import FeedbackReviewPage from "./Components/feedback/FeedbackReviewPage"; function Home() { const [showTop, setShowTop] = useState(false); @@ -136,6 +137,7 @@ function App() { } /> } /> } /> + } /> ); diff --git a/frontend/src/Components/Contributors.jsx b/frontend/src/Components/Contributors.jsx index 6328cbc..56f40dd 100644 --- a/frontend/src/Components/Contributors.jsx +++ b/frontend/src/Components/Contributors.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { FaArrowRight } from "react-icons/fa6"; +import { FaArrowRight } from "react-icons/fa"; import { useNavigate } from "react-router-dom"; const ContributorsSection = () => { diff --git a/frontend/src/Components/DashBoard/GithubRepoCard.jsx b/frontend/src/Components/DashBoard/GithubRepoCard.jsx new file mode 100644 index 0000000..ecfb7ef --- /dev/null +++ b/frontend/src/Components/DashBoard/GithubRepoCard.jsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { Github, Star, GitFork, Clock } from 'lucide-react'; +import CardWrapper from './CardWrapper'; + +/** + * Component to display a list of GitHub repositories + */ +export default function GithubRepoCard({ repositories = [], className = '' }) { + // Format the update time to a readable string + const formatUpdateTime = (dateString) => { + if (!dateString) return ''; + + const date = new Date(dateString); + const now = new Date(); + const diffMs = now - date; + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + return 'Today'; + } else if (diffDays === 1) { + return 'Yesterday'; + } else if (diffDays < 7) { + return `${diffDays} days ago`; + } else if (diffDays < 30) { + const weeks = Math.floor(diffDays / 7); + return `${weeks} ${weeks === 1 ? 'week' : 'weeks'} ago`; + } else { + const months = Math.floor(diffDays / 30); + return `${months} ${months === 1 ? 'month' : 'months'} ago`; + } + }; + + // Language color mapping + const languageColors = { + JavaScript: '#f1e05a', + TypeScript: '#3178c6', + HTML: '#e34c26', + CSS: '#563d7c', + Python: '#3572A5', + Java: '#b07219', + 'C#': '#178600', + PHP: '#4F5D95', + Ruby: '#701516', + Go: '#00ADD8', + Swift: '#F05138', + Kotlin: '#A97BFF', + Rust: '#dea584', + Dart: '#00B4AB', + // Add more languages as needed + default: '#cccccc' + }; + + return ( + +
+ +

+ Repositories +

+
+ + {repositories.length === 0 ? ( +

+ No repositories found. Sync GitHub data to see your repositories. +

+ ) : ( +
+ {repositories.map((repo) => ( +
+ + {repo.name} + + + {repo.description && ( +

+ {repo.description} +

+ )} + +
+ {repo.language && ( +
+ + {repo.language} +
+ )} + +
+ + {repo.stargazers_count} +
+ +
+ + {repo.forks_count} +
+ +
+ + {formatUpdateTime(repo.updated_at)} +
+
+
+ ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/Components/DashBoard/Sidebar.jsx b/frontend/src/Components/DashBoard/Sidebar.jsx index 2b3722b..3e7e6dc 100644 --- a/frontend/src/Components/DashBoard/Sidebar.jsx +++ b/frontend/src/Components/DashBoard/Sidebar.jsx @@ -1,9 +1,11 @@ import React from "react"; -import { CheckSquare, Clock, Settings, Menu } from "lucide-react"; +import { CheckSquare, Clock, Settings, Menu, MessageSquare } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { Sheet, SheetContent, SheetTrigger } from "@/Components/ui/sheet"; import { Button } from "@/Components/ui/button"; +import { useFeedback } from "@/context/FeedbackContext"; +// Define menu items const menuItems = [ { icon: CheckSquare, label: "To do list", path: "/todo" }, { icon: Clock, label: "Pomodoro", path: "/pomodoro" }, @@ -12,6 +14,7 @@ const menuItems = [ export default function Sidebar() { const navigate = useNavigate(); + const { openFeedbackPopup } = useFeedback(); return ( <> @@ -39,6 +42,16 @@ export default function Sidebar() { {label} ))} + + {/* Feedback Button */} + @@ -58,6 +71,16 @@ export default function Sidebar() { {label} ))} + + {/* Feedback Button */} + diff --git a/frontend/src/Components/Dashboard.jsx b/frontend/src/Components/Dashboard.jsx index 212d66a..870a88b 100644 --- a/frontend/src/Components/Dashboard.jsx +++ b/frontend/src/Components/Dashboard.jsx @@ -8,10 +8,14 @@ import GoalsCard from "./DashBoard/GoalsCard"; import TimeSpentCard from "./DashBoard/TimeSpentCard"; import ActivityHeatmap from "./DashBoard/ActivityHeatMap"; import NotesCard from "./DashBoard/NotesCard"; +import FeedbackController from "./feedback/FeedbackController"; import { useNavigate } from "react-router-dom"; import { Card } from "@/Components/ui/Card"; import { ScrollArea } from "@/Components/ui/scroll-area"; import { Alert, AlertDescription, AlertTitle } from "@/Components/ui/alert"; +import { Button } from "@/Components/ui/button"; +import { MessageSquare } from "lucide-react"; +import { useFeedback } from "@/context/FeedbackContext"; export default function Dashboard() { const [profile, setProfile] = useState(null); @@ -19,6 +23,7 @@ export default function Dashboard() { const [error, setError] = useState(null); const [goals, setGoals] = useState([]); const navigate = useNavigate(); + const { openFeedbackPopup } = useFeedback(); const getAuthToken = () => localStorage.getItem("token"); @@ -181,6 +186,9 @@ export default function Dashboard() {
+ {/* Add FeedbackController with the user profile */} + +
{/* Row 1 */} diff --git a/frontend/src/Components/Navbar/Navbar.jsx b/frontend/src/Components/Navbar/Navbar.jsx index df3a19c..8d0c486 100644 --- a/frontend/src/Components/Navbar/Navbar.jsx +++ b/frontend/src/Components/Navbar/Navbar.jsx @@ -3,11 +3,14 @@ import React, { useEffect, useState } from "react"; import { UserCircle, Clock, + Home, Sparkle, Info, Github, Phone, HelpCircle, + MessageCircle, + MessageSquarePlus } from "lucide-react"; import { FloatingNav } from "../ui/floating-navbar"; import { Link, useNavigate } from "react-router-dom"; @@ -15,31 +18,12 @@ import DarkModeToggle from "../ui/DarkModeToggle"; import { useTimer } from "../../context/TimerContext"; const publicNavItems = [ - { - name: "Features", - link: "#features", - icon: , - }, - { - name: "About us", - link: "#about", - icon: , - }, - { - name: "Github", - link: "https://github.com/DevSyncx/DevSync.git", - icon: , - }, - { - name: "Contact Us", - link: "#contact", - icon: , - }, - { - name: "FAQ", - link: "#faq", - icon: , - }, + { name: "Home", link: "/", icon: }, + { name: "Features", link: "#features", icon: }, + { name: "About us", link: "#about", icon: }, + { name: "Github", link: "https://github.com/DevSyncx/DevSync.git", icon: }, + { name: "Contact Us", link: "#contact", icon: }, + { name: "FAQ", link: "#faq", icon: }, ]; const Navbar = () => { diff --git a/frontend/src/Components/auth/Login.jsx b/frontend/src/Components/auth/Login.jsx index 893a224..4fc5452 100644 --- a/frontend/src/Components/auth/Login.jsx +++ b/frontend/src/Components/auth/Login.jsx @@ -246,6 +246,7 @@ const Login = () => { {/* Social Login */}
+ ); +} \ No newline at end of file diff --git a/frontend/src/Components/feedback/FeedbackController.jsx b/frontend/src/Components/feedback/FeedbackController.jsx new file mode 100644 index 0000000..3e8b53c --- /dev/null +++ b/frontend/src/Components/feedback/FeedbackController.jsx @@ -0,0 +1,144 @@ +import React, { useEffect, useRef } from "react"; +import FeedbackPopup from "./FeedbackPopup"; +import { useFeedback } from "../../context/FeedbackContext"; + +const FEEDBACK_INTERVAL_DAYS = 5; // Number of days between feedback prompts +const FEEDBACK_STORAGE_KEY = "devSync_feedback_state"; + +export default function FeedbackController({ user }) { + const { showFeedbackPopup, openFeedbackPopup, closeFeedbackPopup } = useFeedback(); + // Use a ref to track if we've already shown the popup in this session + const hasTriggeredPopup = useRef(false); + + // Check if we should show feedback popup based on last shown date + useEffect(() => { + // Only run this if user is logged in + if (!user) { + return; + } + + // Get a reliable user ID from the user object structure + // Check common properties in the profile object for a usable ID + const userId = user.id || user.githubId || user._id || user.user || + (user.user && user.user._id) || + (typeof user === 'string' ? user : null); + + if (!userId) { + console.error("%c FeedbackController: Unable to determine user ID", "background: red; color: white; padding: 4px; font-weight: bold;", user); + return; + } + + // Use the determined user ID + const effectiveUserId = userId; + + // Check local storage for feedback state + const storedState = localStorage.getItem(FEEDBACK_STORAGE_KEY); + const feedbackState = storedState ? JSON.parse(storedState) : null; + + const shouldShowFeedback = () => { + // For debugging, log what's in localStorage + // If no previous feedback state, show the popup + if (!feedbackState) { + return true; + } + + // If user ID has changed, show the popup + if (feedbackState.userId !== effectiveUserId) { + return true; + } + + // Check if enough time has passed since last feedback + const lastShownDate = new Date(feedbackState.lastShown); + const currentDate = new Date(); + + // Calculate days since last shown + const daysSinceLastShown = Math.floor( + (currentDate - lastShownDate) / (1000 * 60 * 60 * 24) + ); + + // Check if enough time has passed since the last feedback + return daysSinceLastShown >= FEEDBACK_INTERVAL_DAYS; + }; + + // Show feedback popup if needed with a slight delay after login + // Only trigger the popup if we haven't shown it already in this session + if (shouldShowFeedback() && !hasTriggeredPopup.current) { + hasTriggeredPopup.current = true; // Mark that we've triggered the popup + + // Show feedback form after a short delay + const timer = setTimeout(() => { + openFeedbackPopup(); + }, 3000); // 3 second delay + + return () => clearTimeout(timer); + } + }, [user, openFeedbackPopup]); + + // Handle feedback submission + const handleFeedbackSubmit = async (feedbackData) => { + try { + // Get authentication tokens from different sources + const jwtToken = localStorage.getItem("token"); + const githubToken = localStorage.getItem("github_token") || sessionStorage.getItem("github_token"); + + // Prepare headers with available authentication + const headers = { + "Content-Type": "application/json" + }; + + if (jwtToken) { + headers["x-auth-token"] = jwtToken; + } + + if (githubToken) { + headers["Authorization"] = `Bearer ${githubToken}`; + } + + // Send feedback to backend + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/feedback`, { + method: "POST", + headers, + body: JSON.stringify(feedbackData), + credentials: "include" + }); + + if (!response.ok) { + throw new Error(`Error: ${response.status}`); + } + + // Close the popup + closeFeedbackPopup(); + + // Get the user ID (support multiple authentication methods) + const submissionUserId = user.id || user.githubId || user._id || + user.user || (user.user && user.user._id) || + (typeof user === 'string' ? user : null) || + "test-user-id"; + + // Update local storage with the new date + localStorage.setItem( + FEEDBACK_STORAGE_KEY, + JSON.stringify({ + userId: submissionUserId, + lastShown: new Date().toISOString(), + }) + ); + + // Show success message + alert("Thank you for your feedback!"); + + } catch (error) { + console.error("Error submitting feedback:", error); + alert("Failed to submit feedback. Please try again later."); + } + }; + + return ( + + ); +} \ No newline at end of file diff --git a/frontend/src/Components/feedback/FeedbackPopup.jsx b/frontend/src/Components/feedback/FeedbackPopup.jsx new file mode 100644 index 0000000..f0f1f52 --- /dev/null +++ b/frontend/src/Components/feedback/FeedbackPopup.jsx @@ -0,0 +1,182 @@ +import React, { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/Components/ui/dialog"; +import StarRating from "@/Components/ui/StarRating"; +import { Textarea } from "@/Components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select"; +import { Checkbox } from "@/Components/ui/checkbox"; +import { Button } from "@/Components/ui/button"; +import { Sparkles } from "lucide-react"; + +const feedbackCategories = [ + { id: "ui", label: "User Interface" }, + { id: "features", label: "Features" }, + { id: "bugs", label: "Bug Reports" }, + { id: "suggestions", label: "Suggestions" }, + { id: "other", label: "Other" } +]; + +export default function FeedbackPopup({ open, onOpenChange, onSubmit, userInfo }) { + const [rating, setRating] = useState(0); + const [comment, setComment] = useState(""); + const [category, setCategory] = useState("other"); + const [isAnonymous, setIsAnonymous] = useState(false); + const [errors, setErrors] = useState({ rating: "", comment: "" }); + + const handleRatingChange = (value) => { + setRating(value); + if (errors.rating) setErrors({ ...errors, rating: "" }); + }; + + const handleCommentChange = (e) => { + setComment(e.target.value); + if (errors.comment && e.target.value.trim().length >= 10) { + setErrors({ ...errors, comment: "" }); + } + }; + + const validate = () => { + const newErrors = {}; + let isValid = true; + + if (rating === 0) { + newErrors.rating = "Please select a rating"; + isValid = false; + } + + if (!comment.trim()) { + newErrors.comment = "Please provide some feedback"; + isValid = false; + } else if (comment.trim().length < 10) { + newErrors.comment = "Feedback should be at least 10 characters long"; + isValid = false; + } + + setErrors(newErrors); + return isValid; + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!validate()) return; + + // If user is not logged in, we'll always treat it as anonymous + const actualIsAnonymous = !userInfo ? true : isAnonymous; + + const feedbackData = { + userId: userInfo?.id || "anonymous", + rating, + comment: comment.trim(), + category, + isAnonymous: actualIsAnonymous, + date: new Date().toISOString() + }; + + onSubmit(feedbackData); + }; + + return ( + + +
+ + + + We'd Love Your Feedback + + + Help us improve DevSync by sharing your thoughts and experience. + + + +
+ {/* Rating */} +
+ +
+ +
+ {errors.rating && ( +

{errors.rating}

+ )} +
+ + {/* Category */} +
+ + +
+ + {/* Comment */} +
+ +