diff --git a/nextstep-backend/src/app.ts b/nextstep-backend/src/app.ts index 371440b..fb7f02a 100644 --- a/nextstep-backend/src/app.ts +++ b/nextstep-backend/src/app.ts @@ -15,6 +15,7 @@ import validateUser from "./middleware/validateUser"; import loadOpenApiFile from "./openapi/openapi_loader"; import resource_routes from './routes/resources_routes'; import resume_routes from './routes/resume_routes'; +import githubRoutes from './routes/github_routes'; const specs = swaggerJsdoc(options); @@ -74,5 +75,6 @@ app.use('/user', usersRoutes); app.use('/resource', resource_routes); app.use('/room', roomsRoutes); app.use('/resume', resume_routes); +app.use('/github', githubRoutes); export { app, corsOptions }; diff --git a/nextstep-backend/src/controllers/github_controller.ts b/nextstep-backend/src/controllers/github_controller.ts new file mode 100644 index 0000000..1df6563 --- /dev/null +++ b/nextstep-backend/src/controllers/github_controller.ts @@ -0,0 +1,93 @@ +import axios from 'axios'; +import { Request, Response } from 'express'; + +const clientId = process.env.GITHUB_CLIENT_ID; +const clientSecret = process.env.GITHUB_CLIENT_SECRET; + +export const handleGitHubOAuth = async (req: Request, res: Response) => { + const { code } = req.body; + + if (!code) { + return res.status(400).json({ error: 'Authorization code is required' }); + } + + try { + const tokenResponse = await axios.post('https://github.com/login/oauth/access_token', { + headers: { 'Content-Type': 'application/json' }, + client_id: clientId, + client_secret: clientSecret, + code: code, + }) as any; + + const tokenData = await tokenResponse.data; + const params = new URLSearchParams(tokenData); + const accessToken = params.get('access_token'); + + if (!accessToken) { + return res.status(400).json({ error: 'Failed to retrieve access token' }); + } + + const userResponse = await axios.get('https://api.github.com/user', { + headers: { Authorization: `Bearer ${accessToken}` }, + }) as {status: number, data: { login?: string }, json: () => Promise}; + + if (userResponse.status !== 200) { + const errorText = await userResponse.data; + return res.status(400).json({ error: errorText }); + } + + const userData = await userResponse.data; + res.json({ username: userData.login }); + } catch (error) { + console.error('Error during GitHub OAuth:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const fetchGitHubRepos = async (req: Request, res: Response) => { + const { username } = req.params; + const { accessToken } = req.query; // Optional access token for authenticated requests + + try { + const apiUrl = `https://api.github.com/users/${username}/repos`; + + const headers = accessToken + ? { Authorization: `Bearer ${accessToken}` } + : undefined; + + const response = await axios.get(apiUrl, { headers }) as { data: any, status: number }; + + if (response.status !== 200) { + return res.status(400).json({ error: `Error fetching repos: ${response.status}` }); + } + + const repos = await response.data; + res.json(repos); + } catch (error) { + console.error('Error fetching repos:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const fetchRepoLanguages = async (req: Request, res: Response) => { + const { repoUrl } = req.query; + + if (!repoUrl) { + return res.status(400).json({ error: 'Repository URL is required' }); + } + + try { + // Convert the repoUrl to the GitHub API URL if necessary + const apiUrl = (repoUrl as string).replace('https://github.com/', 'https://api.github.com/repos/'); + const response = await axios.get(`${apiUrl}/languages`); + + if (response.status !== 200) { + return res.status(400).json({ error: `Error fetching languages: ${response.statusText}` }); + } + + res.json(response.data); + } catch (error) { + console.error('Error fetching languages:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/nextstep-backend/src/controllers/resume_controller.ts b/nextstep-backend/src/controllers/resume_controller.ts index 4ef1d63..27dee76 100644 --- a/nextstep-backend/src/controllers/resume_controller.ts +++ b/nextstep-backend/src/controllers/resume_controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { config } from '../config/config'; import fs from 'fs'; import path from 'path'; -import { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume } from '../services/resume_service'; +import { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume, parseResumeFields } from '../services/resume_service'; import multer from 'multer'; import { CustomRequest } from "types/customRequest"; import { handleError } from "../utils/handle_error"; @@ -93,4 +93,18 @@ const generateResume = async (req: Request, res: Response) => { } }; -export default { getResumeScore, getStreamResumeScore, getTemplates, generateResume }; \ No newline at end of file + +const parseResume = async (req: Request, res: Response) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'No resume file uploaded' }); + } + const parsed = await parseResumeFields(req.file.buffer, req.file.originalname); + return res.status(200).json(parsed); + } catch (err: any) { + console.error('Error parsing resume:', err); + return handleError(err, res); + } + }; + +export default { parseResume, getResumeScore, getStreamResumeScore, getTemplates, generateResume }; \ No newline at end of file diff --git a/nextstep-backend/src/routes/github_routes.ts b/nextstep-backend/src/routes/github_routes.ts new file mode 100644 index 0000000..5969109 --- /dev/null +++ b/nextstep-backend/src/routes/github_routes.ts @@ -0,0 +1,10 @@ +import express from 'express'; +import { handleGitHubOAuth, fetchGitHubRepos, fetchRepoLanguages } from '../controllers/github_controller'; + +const router = express.Router(); + +router.post('/oauth', handleGitHubOAuth); +router.get('/repos/:username', fetchGitHubRepos); +router.get('/languages', fetchRepoLanguages); + +export default router; diff --git a/nextstep-backend/src/routes/resume_routes.ts b/nextstep-backend/src/routes/resume_routes.ts index 017b26a..b365b02 100644 --- a/nextstep-backend/src/routes/resume_routes.ts +++ b/nextstep-backend/src/routes/resume_routes.ts @@ -1,6 +1,9 @@ import express, { Request, Response } from 'express'; import Resume from '../controllers/resume_controller'; import { CustomRequest } from "types/customRequest"; +import multer from 'multer'; + +const upload = multer(); const router = express.Router(); @@ -12,4 +15,6 @@ router.get('/templates', Resume.getTemplates); router.post('/generate', Resume.generateResume); +router.post('/parseResume', upload.single('resume'), Resume.parseResume); + export default router; \ No newline at end of file diff --git a/nextstep-backend/src/services/resume_service.ts b/nextstep-backend/src/services/resume_service.ts index f5ce97e..dd33ed0 100644 --- a/nextstep-backend/src/services/resume_service.ts +++ b/nextstep-backend/src/services/resume_service.ts @@ -8,6 +8,14 @@ import pdfParse from 'pdf-parse'; import AdmZip from 'adm-zip'; import { DOMParser, XMLSerializer } from 'xmldom'; +export interface ParsedResume { + aboutMe: string; + skills: string[]; + roleMatch: string; + experience:string[]; + education?: string[]; +} + const SYSTEM_TEMPLATE = `You are a very experienced ATS (Application Tracking System) bot with a deep understanding named Bob the Resume builder. You will review resumes with or without job descriptions. You are an expert in resume evaluation and provide constructive feedback with dynamic evaluation. @@ -386,4 +394,51 @@ Rules: } }; -export { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume }; \ No newline at end of file + +/** + * Extracts raw text from the uploaded resume buffer, + * prompts the AI to return { aboutMe, skills[], roleMatch, experience[] } as JSON. + */ +const parseResumeFields = async ( + fileBuffer: Buffer, + originalName: string + ): Promise => { + // 1) Extract text + const ext = path.extname(originalName).toLowerCase(); + let text: string; + if (ext === '.pdf') { + const data = await pdfParse(fileBuffer); + text = data.text; + } else { + // mammoth supports buffer input + const { value } = await mammoth.extractRawText({ buffer: fileBuffer }); + text = value; + } + + // 2) Build the extraction prompt + const prompt = ` + Extract from this resume the following fields as JSON: + • "aboutMe": a 1–2 sentence self-summary. + • "skills": an array of technical skills. + • "roleMatch": one-sentence best-fit role suggestion. + • "experience": an array of 3–5 bullet points of key achievements. + + Resume text: + --- + ${text} + --- + Respond with a single JSON object and nothing else. The json object should begin directly with parentheses and have the following structure: {"a": "value", "b": "value", ...} + `; + + // 3) Call your Chat AI + const aiResponse = await chatWithAI( + SYSTEM_TEMPLATE, // you can reuse your existing SYSTEM_TEMPLATE or define a new one + [prompt] + ); + + // 4) Parse & return + const parsed = JSON.parse(aiResponse.trim().replace("```json", "").replace("```", "")) as ParsedResume; + return parsed; + }; + +export { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume, parseResumeFields }; \ No newline at end of file diff --git a/nextstep-frontend/src/App.tsx b/nextstep-frontend/src/App.tsx index 4eb7bc9..525d2b2 100644 --- a/nextstep-frontend/src/App.tsx +++ b/nextstep-frontend/src/App.tsx @@ -4,7 +4,7 @@ import Login from './pages/Login'; import Register from './pages/Register'; import Profile from './pages/Profile'; import './App.css' -import Dashboard from './pages/Dashboard'; +import Feed from './pages/Feed'; import Footer from './components/Footer'; import RequireAuth from './hoc/RequireAuth'; import NewPost from './pages/NewPost'; @@ -13,6 +13,7 @@ import Chat from './pages/Chat'; import Resume from './pages/Resume'; import TopBar from './components/TopBar'; import Layout from './components/Layout'; +import MainDashboard from './pages/MainDashboard'; const App: React.FC = () => { return ( @@ -22,12 +23,13 @@ const App: React.FC = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> } /> } /> + } /> } /> diff --git a/nextstep-frontend/src/components/Footer.css b/nextstep-frontend/src/components/Footer.css index 404eb5f..16fe36d 100644 --- a/nextstep-frontend/src/components/Footer.css +++ b/nextstep-frontend/src/components/Footer.css @@ -1,5 +1,5 @@ .footer { - background-color: var(--color-5); + background-color: #233752; color: var(--color-2); padding: 10px 20px; text-align: center; diff --git a/nextstep-frontend/src/components/TopBar.tsx b/nextstep-frontend/src/components/TopBar.tsx index 2a3350d..c717820 100644 --- a/nextstep-frontend/src/components/TopBar.tsx +++ b/nextstep-frontend/src/components/TopBar.tsx @@ -1,7 +1,7 @@ import React, { useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { AppBar, Toolbar, IconButton, Tooltip, Box } from '@mui/material'; -import { Home, Person, Message, Logout, DocumentScannerTwoTone } from '@mui/icons-material'; +import { Home, Person, Message, Logout, DocumentScannerTwoTone, Feed } from '@mui/icons-material'; import {getUserAuth, removeUserAuth} from "../handlers/userAuth.ts"; import api from "../serverApi.ts"; @@ -20,16 +20,21 @@ const TopBar: React.FC = () => { }; return ( - + - navigate('/dashboard')} sx={{ mx: 1 }}> + navigate('/main-dashboard')} sx={{ mx: 1 }}> - - navigate('/profile')} sx={{ mx: 1 }}> - + + navigate('/resume')} sx={{ mx: 1 }}> + + + + + navigate('/feed')} sx={{ mx: 1 }}> + @@ -37,9 +42,9 @@ const TopBar: React.FC = () => { - - navigate('/resume')} sx={{ mx: 1 }}> - + + navigate('/profile')} sx={{ mx: 1 }}> + diff --git a/nextstep-frontend/src/handlers/githubAuth.ts b/nextstep-frontend/src/handlers/githubAuth.ts new file mode 100644 index 0000000..5961d59 --- /dev/null +++ b/nextstep-frontend/src/handlers/githubAuth.ts @@ -0,0 +1,72 @@ +import api from "../serverApi.ts"; + +// Function to fetch GitHub repositories by username +export const fetchGitHubRepos = async (username: string) => { + try { + const response = await api.get(`github/repos/${username}`) as any; + if (response.status !== 200) { + throw new Error(`Error fetching repos: ${response.statusText}`); + } + return response.data; + } catch (error) { + console.error(error); + return []; + } +}; + +// Function to connect to GitHub by username +export const connectToGitHub = async (username: string) => { + if (!username) { + throw new Error('Username is required to connect to GitHub.'); + } + return await fetchGitHubRepos(username); +}; + +// Function to initiate GitHub OAuth login (redirect only) +export const initiateGitHubOAuth = () => { + const clientId = import.meta.env.VITE_GITHUB_CLIENT_ID; // Use environment variable + const redirectUri = import.meta.env.VITE_GITHUB_REDIRECT_URI; // Use environment variable + const scope = 'public_repo'; // Only read access to repositories + const authUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}`; + window.location.href = authUrl; // Redirect the user to GitHub's authorization page +}; + +// Function to handle GitHub OAuth token exchange and fetching user details +export const handleGitHubOAuth = async (code: string) => { + try { + const response = await api.post('github/oauth', { + headers: { 'Content-Type': 'application/json' }, + code: code, + }) as {data: {username?: string, error?: string}, status: number}; + + if (response.status !== 200) { + const errorText = await response.data.error; + console.error('Backend OAuth Error:', errorText); + throw new Error('Failed to authenticate with GitHub'); + } + else if (!response.data.username) { + console.error('Backend OAuth Error:', response.data.error); + throw new Error('Failed to retrieve user information from GitHub'); + } + + return response.data.username; + } catch (error) { + const err : Error = error as Error; + console.error('Error during GitHub OAuth:', error); + throw new Error('Internal server error: ' + err.message); + } +}; + +// Function to fetch languages for a specific repository +export const fetchRepoLanguages = async (repoUrl: string) => { + try { + const response = await api.get(`github/languages?repoUrl=${encodeURIComponent(repoUrl)}`) as any; + if (response.status !== 200) { + throw new Error(`Error fetching languages: ${response.statusText}`); + } + return await response.data; + } catch (error) { + console.error(error); + return {}; + } +}; \ No newline at end of file diff --git a/nextstep-frontend/src/pages/Dashboard.tsx b/nextstep-frontend/src/pages/Feed.tsx similarity index 98% rename from nextstep-frontend/src/pages/Dashboard.tsx rename to nextstep-frontend/src/pages/Feed.tsx index 93ad626..6ba9901 100644 --- a/nextstep-frontend/src/pages/Dashboard.tsx +++ b/nextstep-frontend/src/pages/Feed.tsx @@ -22,13 +22,13 @@ import { Container, } from "@mui/material"; import { ThumbUp, Message, Delete } from '@mui/icons-material'; -import { Post } from "../models/Post"; -import api from "../serverApi"; +import { Post } from "../models/Post.tsx"; +import api from "../serverApi.ts"; import {getUserAuth} from "../handlers/userAuth.ts"; import defaultProfileImage from '../../assets/defaultProfileImage.jpg'; // Import the default profile image -const Dashboard: React.FC = () => { +const Feed: React.FC = () => { const navigate = useNavigate(); const [posts, setPosts] = useState([]); const [commentsCount, setCommentsCount] = useState<{ [key: string]: number }>({}); @@ -322,4 +322,4 @@ const Dashboard: React.FC = () => { ); }; -export default Dashboard; \ No newline at end of file +export default Feed; \ No newline at end of file diff --git a/nextstep-frontend/src/pages/Login.tsx b/nextstep-frontend/src/pages/Login.tsx index fec6550..741691f 100644 --- a/nextstep-frontend/src/pages/Login.tsx +++ b/nextstep-frontend/src/pages/Login.tsx @@ -32,7 +32,7 @@ const Login: React.FC = () => { setUserAuth(res.data); - navigate("/dashboard"); + navigate("/main-dashboard"); } catch (error) { console.error("Google login failed:", error); setError("Google login failed."); @@ -51,7 +51,7 @@ const Login: React.FC = () => { // Handle successful login, e.g., save tokens, redirect, etc. setUserAuth(response.data) - navigate('/dashboard'); // Redirect to dashboard or another page after login + navigate('/main-dashboard'); // Redirect to dashboard or another page after login } catch (error) { // Handle login error diff --git a/nextstep-frontend/src/pages/MainDashboard.tsx b/nextstep-frontend/src/pages/MainDashboard.tsx new file mode 100644 index 0000000..9903df3 --- /dev/null +++ b/nextstep-frontend/src/pages/MainDashboard.tsx @@ -0,0 +1,386 @@ +import React, { useState, useEffect } from 'react'; +import { + Container, + Grid, + Box, + Typography, + TextField, + Button, + Chip, + Stack, + Avatar, + Divider, + Autocomplete, + FormControl, + InputLabel, + Select, + MenuItem, + CircularProgress, + IconButton, + Tooltip +} from '@mui/material'; +import { + GitHub, + LinkedIn, + Person as PersonIcon, + Work as WorkIcon, + Build as BuildIcon, + UploadFile as UploadFileIcon +} from '@mui/icons-material'; +import { + connectToGitHub, + initiateGitHubOAuth, + fetchRepoLanguages, + handleGitHubOAuth +} from '../handlers/githubAuth'; +import api from '../serverApi'; + +const roles = [ + 'Software Engineer', 'Frontend Developer', 'Backend Developer', + 'Full Stack Developer', 'DevOps Engineer', 'Product Manager', 'UI/UX Designer' +]; + +const skillsList = [ + 'React', 'JavaScript', 'TypeScript', 'Python', 'Java', 'Node.js', + 'Express', 'MongoDB', 'AWS', 'Docker', 'Kubernetes', 'Git', 'Agile' +]; + +const MainDashboard: React.FC = () => { + const [aboutMe, setAboutMe] = useState(() => localStorage.getItem('aboutMe') || ''); + const [skills, setSkills] = useState(() => JSON.parse(localStorage.getItem('skills') || '[]')); + const [newSkill, setNewSkill] = useState(''); + const [selectedRole, setSelectedRole] = useState(() => localStorage.getItem('selectedRole') || ''); + const [repos, setRepos] = useState<{ id: number; name: string; html_url: string }[]>([]); + const [useOAuth, setUseOAuth] = useState(true); + const [showAuthOptions, setShowAuthOptions] = useState(false); + + // AI-resume state + const [parsing, setParsing] = useState(false); + const [resumeExperience, setResumeExperience] = useState([]); + const [roleMatch, setRoleMatch] = useState(''); + const [resumeFileName, setResumeFileName] = useState(''); + + // Skills toggle + const [showAllSkills, setShowAllSkills] = useState(false); + const SKILL_DISPLAY_LIMIT = 6; + const shouldShowToggle = skills.length > SKILL_DISPLAY_LIMIT; + + // Persist to localStorage + useEffect(() => { localStorage.setItem('aboutMe', aboutMe); }, [aboutMe]); + useEffect(() => { localStorage.setItem('skills', JSON.stringify(skills)); }, [skills]); + useEffect(() => { localStorage.setItem('selectedRole', selectedRole); }, [selectedRole]); + + // Handle GitHub OAuth callback + useEffect(() => { + const code = new URLSearchParams(window.location.search).get('code'); + if (code) { + (async () => { + try { + const username = await handleGitHubOAuth(code); + const fetched = await connectToGitHub(username); + setRepos(fetched); + mergeRepoLanguages(fetched); + } catch (e) { + console.error(e); + } + })(); + } + }, []); + + const mergeRepoLanguages = async (fetchedRepos: typeof repos) => { + const langSet = new Set(skills); + for (const repo of fetchedRepos) { + const langs = await fetchRepoLanguages(repo.html_url); + Object.keys(langs).forEach(lang => langSet.add(lang)); + } + setSkills(Array.from(langSet)); + }; + + const handleAddSkill = (skill: string) => { + const trimmed = skill.trim(); + if (!trimmed || skills.includes(trimmed)) return; + setSkills(prev => [trimmed, ...prev]); + setNewSkill(''); + }; + + const handleDeleteSkill = (skillToDelete: string) => { + setSkills(prev => prev.filter(s => s !== skillToDelete)); + }; + + const handleGitHubConnect = async () => { + if (!showAuthOptions) return setShowAuthOptions(true); + try { + if (useOAuth) initiateGitHubOAuth(); + else { + const username = prompt('Enter GitHub username:'); + if (!username) return alert('Username required'); + const fetched = await connectToGitHub(username); + setRepos(fetched); + mergeRepoLanguages(fetched); + } + } catch (e) { + console.error(e); + } finally { + setShowAuthOptions(false); + } + }; + + // Upload & parse resume + const handleResumeUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setResumeFileName(file.name); + setParsing(true); + const form = new FormData(); + form.append('resume', file); + try { + const res = await api.post('/resume/parseResume', form, { + headers: { 'Content-Type': 'multipart/form-data' } + }); + const { aboutMe: aiAbout, skills: aiSkills, roleMatch: aiRole, experience: aiExp } = res.data; + setAboutMe(aiAbout); + setSkills(aiSkills); + setRoleMatch(aiRole); + setResumeExperience(aiExp); + } catch (err) { + console.error(err); + alert('Failed to parse resume.'); + } finally { + setParsing(false); + } + }; + + return ( + + + {/* Left Column */} + + + {/* About Me Section */} + + {/* Upload icon & filename above the title, right-aligned */} + + + + + + + {resumeFileName && ( + + {resumeFileName} + + )} + {parsing && } + + + {/* Header */} + + + + About Me + + + + {/* Content */} + setAboutMe(e.target.value)} + /> + + + {/* Desired Role */} + + + + + Desired Role + + + setSelectedRole(val)} + renderInput={params => ( + + )} + /> + + + {/* Skills */} + + + + + Skills + + + + + {(showAllSkills ? skills : skills.slice(0, SKILL_DISPLAY_LIMIT)).map(skill => ( + handleDeleteSkill(skill)} /> + ))} + + {shouldShowToggle && ( + + )} + + setNewSkill(val)} + onChange={(_, val) => val && handleAddSkill(val)} + renderInput={params => ( + e.key === 'Enter' && handleAddSkill(newSkill)} + /> + )} + sx={{ flexGrow: 1 }} + /> + + + + + {/* Suggested Role Match */} + {roleMatch && ( + + + + + Suggested Role Match + + + {roleMatch} + + )} + + {/* Experience */} + {resumeExperience.length > 0 && ( + + + + + Experience + + + + {resumeExperience.map((exp, i) => ( + {exp} + ))} + + + )} + + + + + {/* Right Column */} + + + + + Connect Accounts + + + {showAuthOptions ? ( + + + Method + + + + + + ) : ( + + + + + )} + + {repos.length > 0 && ( + + + Repositories: + + + {repos.map(repo => ( + + ))} + + + )} + + + + + ); +}; + +export default MainDashboard; diff --git a/nextstep-frontend/src/pages/NewPost.tsx b/nextstep-frontend/src/pages/NewPost.tsx index 1e50e14..176b4b7 100644 --- a/nextstep-frontend/src/pages/NewPost.tsx +++ b/nextstep-frontend/src/pages/NewPost.tsx @@ -50,7 +50,7 @@ const NewPost: React.FC = () => { content: updatedContent, }); - navigate('/dashboard'); // Redirect to dashboard after successful post creation + navigate('/feed'); // Redirect to feed after successful post creation } catch (error) { console.error('Error creating post:', error); } @@ -119,10 +119,10 @@ const NewPost: React.FC = () => { fullWidth variant="outlined" color="secondary" - onClick={() => navigate('/dashboard')} + onClick={() => navigate('/feed')} sx={{ mt: 2 }} > - Back to Dashboard + Back to Feed diff --git a/nextstep-frontend/src/pages/PostDetails.tsx b/nextstep-frontend/src/pages/PostDetails.tsx index 35b965d..9271dbf 100644 --- a/nextstep-frontend/src/pages/PostDetails.tsx +++ b/nextstep-frontend/src/pages/PostDetails.tsx @@ -171,7 +171,7 @@ const PostDetails: React.FC = () => { const handleDeletePost = async () => { try { await api.delete(`/post/${postId}`); - navigate('/dashboard'); + navigate('/feed'); } catch (err) { console.error('Failed to delete post:', err); } @@ -197,7 +197,7 @@ const PostDetails: React.FC = () => { - navigate('/dashboard')}> + navigate('/feed')}>