From b70b5ab30f4b06885c2bc50999e0056da3b94fa7 Mon Sep 17 00:00:00 2001 From: Tamir Shoval <98471911+Tamir26@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:25:29 +0200 Subject: [PATCH 1/6] Feature/fix UI and seeding (#51) * Secure public user profiles by omitting emails and refresh tokens from external API calls * feat: improve user search intent and expose Swagger/Jest UI * feat: add mock data seeding script * feat: fix production seeding and UI document persistence * feat: finalize production-standard project structure for professor review From f29c03ceba6b7d53212333cc11f7c6dee951c9ed Mon Sep 17 00:00:00 2001 From: Tamir Shoval <98471911+Tamir26@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:42:22 +0200 Subject: [PATCH 2/6] Feature/fix alerts trigger persistence (#52) * Release: Merge main into production (#26) * Production readiness & CI/CD deployment setup * Update deployment workflow to support GitHub Secrets and Variables division (#22) * Feature/automated production deployment (#23) * Update deployment workflow to support GitHub Secrets and Variables division * Implement automated main-to-prod PRs and self-hosted production deployment workflow * Optimize production deployment: remove redundant server-side testing for low-resource environment * Add CI/CD flow documentation to README and finalize production branch naming * Update CI workflow to use real GitHub Secrets and Variables * Disable Facebook OAuth strategy, routes, and documentation * Fix CI timeout: use local MongoDB service for tests * Fix frontend build: install client dependencies before building * Fix/automated production deployment (#24) * Update deployment workflow to support GitHub Secrets and Variables division * Implement automated main-to-prod PRs and self-hosted production deployment workflow * Optimize production deployment: remove redundant server-side testing for low-resource environment * Add CI/CD flow documentation to README and finalize production branch naming * Update CI workflow to use real GitHub Secrets and Variables * Disable Facebook OAuth strategy, routes, and documentation * Fix CI timeout: use local MongoDB service for tests * Fix frontend build: install client dependencies before building * Fix pipeline permissions and remove redundant deploy workflow * Fix/automated production deployment (#25) * Update deployment workflow to support GitHub Secrets and Variables division * Implement automated main-to-prod PRs and self-hosted production deployment workflow * Optimize production deployment: remove redundant server-side testing for low-resource environment * Add CI/CD flow documentation to README and finalize production branch naming * Update CI workflow to use real GitHub Secrets and Variables * Disable Facebook OAuth strategy, routes, and documentation * Fix CI timeout: use local MongoDB service for tests * Fix frontend build: install client dependencies before building * Fix pipeline permissions and remove redundant deploy workflow * Allow custom PR_TOKEN and update documentation for permission fix * Release: Merge main into production (#28) * Production readiness & CI/CD deployment setup * Update deployment workflow to support GitHub Secrets and Variables division (#22) * Feature/automated production deployment (#23) * Update deployment workflow to support GitHub Secrets and Variables division * Implement automated main-to-prod PRs and self-hosted production deployment workflow * Optimize production deployment: remove redundant server-side testing for low-resource environment * Add CI/CD flow documentation to README and finalize production branch naming * Update CI workflow to use real GitHub Secrets and Variables * Disable Facebook OAuth strategy, routes, and documentation * Fix CI timeout: use local MongoDB service for tests * Fix frontend build: install client dependencies before building * Fix/automated production deployment (#24) * Update deployment workflow to support GitHub Secrets and Variables division * Implement automated main-to-prod PRs and self-hosted production deployment workflow * Optimize production deployment: remove redundant server-side testing for low-resource environment * Add CI/CD flow documentation to README and finalize production branch naming * Update CI workflow to use real GitHub Secrets and Variables * Disable Facebook OAuth strategy, routes, and documentation * Fix CI timeout: use local MongoDB service for tests * Fix frontend build: install client dependencies before building * Fix pipeline permissions and remove redundant deploy workflow * Fix/automated production deployment (#25) * Update deployment workflow to support GitHub Secrets and Variables division * Implement automated main-to-prod PRs and self-hosted production deployment workflow * Optimize production deployment: remove redundant server-side testing for low-resource environment * Add CI/CD flow documentation to README and finalize production branch naming * Update CI workflow to use real GitHub Secrets and Variables * Disable Facebook OAuth strategy, routes, and documentation * Fix CI timeout: use local MongoDB service for tests * Fix frontend build: install client dependencies before building * Fix pipeline permissions and remove redundant deploy workflow * Allow custom PR_TOKEN and update documentation for permission fix * Fix/automated production deployment (#27) * Update deployment workflow to support GitHub Secrets and Variables division * Implement automated main-to-prod PRs and self-hosted production deployment workflow * Optimize production deployment: remove redundant server-side testing for low-resource environment * Add CI/CD flow documentation to README and finalize production branch naming * Update CI workflow to use real GitHub Secrets and Variables * Disable Facebook OAuth strategy, routes, and documentation * Fix CI timeout: use local MongoDB service for tests * Fix frontend build: install client dependencies before building * Fix pipeline permissions and remove redundant deploy workflow * Allow custom PR_TOKEN and update documentation for permission fix * Restore automatic hotfix sync with circular loop protection * only PRs to main would trigger the CI (#29) * Fix: use 'docker compose' (v2) instead of legacy 'docker-compose' * Fix Docker permissions (sudo) and remove obsolete version key * Fix hotfix sync: create PR instead of direct push (main is protected) * Remove sudo from docker commands (node65 now in docker group) * Fix SPA fallback: allow /auth/success to reach React frontend * Feature/UI and auth fixes (#33) (#34) * Improve README with full app description and CI/CD architecture docs * Fix Google OAuth login loop and SPA fallback for /auth/success * Fix profile image fallback, fix Layout syntax, add DD-MM-YY-HH-mm to PR title (#37) (#38) * Release: Merge main into production [17-03-26-10-07] (#40) * Fix profile image fallback, fix Layout syntax, add DD-MM-YY-HH-mm to PR title (#37) * Fix SPA fallback ordering and AI provider fallback on 401 (#39) * Release: Merge main into production [17-03-26-10-20] (#42) * Fix profile image fallback, fix Layout syntax, add DD-MM-YY-HH-mm to PR title (#37) * Fix SPA fallback ordering and AI provider fallback on 401 (#39) * Fix image persistency by adding app_uploads Docker volume (#41) * Release: Merge main into production [17-03-26-13-37] (#44) * Fix profile image fallback, fix Layout syntax, add DD-MM-YY-HH-mm to PR title (#37) * Fix SPA fallback ordering and AI provider fallback on 401 (#39) * Fix image persistency by adding app_uploads Docker volume (#41) * Fix AI smart search strictness bug by using OR instead of AND (#43) * Secure public user profiles by omitting emails and refresh tokens from external API calls * Release: Merge main into production [17-03-26-15-15] (#46) * Fix profile image fallback, fix Layout syntax, add DD-MM-YY-HH-mm to PR title (#37) * Fix SPA fallback ordering and AI provider fallback on 401 (#39) * Fix image persistency by adding app_uploads Docker volume (#41) * Fix AI smart search strictness bug by using OR instead of AND (#43) * Secure public user profiles by omitting emails and refresh tokens from external API calls (#45) * feat: improve user search intent and expose Swagger/Jest UI * feat: add mock data seeding script * Release: Merge main into production [17-03-26-17-58] (#48) * Fix profile image fallback, fix Layout syntax, add DD-MM-YY-HH-mm to PR title (#37) * Fix SPA fallback ordering and AI provider fallback on 401 (#39) * Fix image persistency by adding app_uploads Docker volume (#41) * Fix AI smart search strictness bug by using OR instead of AND (#43) * Secure public user profiles by omitting emails and refresh tokens from external API calls (#45) * Feature/search swagger jest (#47) * Secure public user profiles by omitting emails and refresh tokens from external API calls * feat: improve user search intent and expose Swagger/Jest UI * feat: add mock data seeding script * feat: fix production seeding and UI document persistence * feat: finalize production-standard project structure for professor review * fix: update CSP to allow external images for mock data * fix: move swagger and test-report routes before SPA fallback * fix: restore swagger endpoints in production and resolve test timeouts in CI * fix: ignore stale test reports and increase test setup timeouts * fix: secure and automate test report refresh in production * fix: remove stale local test report from repository * fix: run tests during docker build stage to generate report reliably * fix: relocate tests to host runner and increase timeouts to 60s for stability * fix: enable pending AI tests by ensuring ENV and secrets are passed to host runner * fix: update AI tests to run if either Gemini or OpenRouter key is present * fix: make AI service key validation more flexible and add production referer * fix: enable experimental-vm-modules for AI test compatibility and switch to jest.config.js * fix: implement robust Avatar component and ensure horizontal navbar layout * fix: restore missing imports and final UI layout tweaks * refactor: split deployment into test and deploy jobs, and improve avatar resilience * fix: add missing useEffect import for Avatar resilience * fix: make OAuth initialization optional and provide secrets to CI test job * fix: include Google OAuth secrets in test job environment * feat: implement professional split-stage pipeline, OAuth resilience, and premium avatar styling * fix: migrate PostDetails and UserProfile to Avatar component * fix: refine navbar layout and hide profile dropdown caret * fix: make alert action icons visible using css color * fix: add text labels to alert actions to ensure visibility * fix: resolve alert persistence and trigger logic for CVX and other stocks * fix: isolate test env from .env variables and rename CI job * fix: resolve AI test environment pollution by removing hardcoded dotenv import * fix: resolve LlamaIndex dynamic import error in tests by enabling experimental-vm-modules --- .github/workflows/ci.yml | 5 +- .github/workflows/deploy-prod.yml | 61 +++- .github/workflows/sync-main.yml | 31 +- Dockerfile | 7 +- ai_test_err.txt | Bin 0 -> 15460 bytes all_tests_out.txt | Bin 0 -> 95578 bytes client/src/components/Avatar.tsx | 81 +++++ client/src/components/CommentSection.tsx | 34 +- client/src/components/Layout.tsx | 52 +-- client/src/components/PostCard.tsx | 18 +- client/src/index.css | 3 +- client/src/pages/Alerts.tsx | 23 +- client/src/pages/PostDetails.tsx | 19 +- client/src/pages/UserProfile.tsx | 26 +- docker-compose.yml | 1 - jest.config.ts => jest.config.js | 3 +- package-lock.json | 26 ++ package.json | 3 +- setup_runner.sh | 40 -- src/app.ts | 38 +- src/config/passport.ts | 60 +-- src/controllers/auth_controller.ts | 3 +- src/models/stock_alert_model.ts | 3 +- src/routes/stock_routes.ts | 4 +- src/services/gemini_service.ts | 18 +- src/services/stock_service.ts | 26 +- src/swagger.ts | 7 +- src/tests/ai.test.ts | 10 +- src/tests/auth.test.ts | 7 +- src/tests/comment.test.ts | 3 + src/tests/post.test.ts | 5 +- src/tests/stock_alert.test.ts | 160 ++++++++ src/tests/user.test.ts | 3 + test-report.html | 445 +---------------------- test_fetch.js | 20 - test_output.txt | Bin 0 -> 9044 bytes test_ping.js | 19 - 37 files changed, 522 insertions(+), 742 deletions(-) create mode 100644 ai_test_err.txt create mode 100644 all_tests_out.txt create mode 100644 client/src/components/Avatar.tsx rename jest.config.ts => jest.config.js (92%) delete mode 100644 setup_runner.sh create mode 100644 src/tests/stock_alert.test.ts delete mode 100644 test_fetch.js create mode 100644 test_output.txt delete mode 100644 test_ping.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cee2f4..cff925a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: jobs: - build-and-test: + test-and-build: runs-on: ubuntu-latest services: @@ -30,7 +30,7 @@ jobs: npm ci --prefix client - name: Run tests - run: npm test + run: NODE_OPTIONS="--experimental-vm-modules" npm test env: MONGO_URI: mongodb://localhost:27017/test_db JWT_SECRET: ${{ secrets.JWT_SECRET }} @@ -41,6 +41,7 @@ jobs: GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + BCRYPT_SALT_ROUNDS: 1 - name: Build (Backend & Frontend) run: npm run build diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 49a1853..d382737 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -6,13 +6,63 @@ on: workflow_dispatch: jobs: - deploy: + run-tests: runs-on: self-hosted - steps: - uses: actions/checkout@v4 - name: Set up Environment + run: | + echo "PORT=${{ vars.PORT || 3000 }}" > .env + echo "NODE_ENV=test" >> .env + echo "MONGO_URI=mongodb://admin:market_watch_pass@127.0.0.1:27017/test_db?authSource=admin" >> .env + echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env + echo "REFRESH_TOKEN_SECRET=${{ secrets.REFRESH_TOKEN_SECRET }}" >> .env + echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" >> .env + echo "GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}" >> .env + echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env + echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env + + - name: Use Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run Tests on Host + run: | + npm install + docker compose up -d mongodb + sleep 15 + MONGO_URI=mongodb://admin:market_watch_pass@127.0.0.1:27017/test_db?authSource=admin \ + GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }} \ + OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }} \ + JWT_SECRET=${{ secrets.JWT_SECRET }} \ + REFRESH_TOKEN_SECRET=${{ secrets.REFRESH_TOKEN_SECRET }} \ + NODE_OPTIONS=--experimental-vm-modules \ + BCRYPT_SALT_ROUNDS=1 npm test + docker compose stop mongodb + + - name: Upload Test Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-report + path: test-report.html + retention-days: 1 + + deploy-app: + needs: run-tests + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + - name: Download Test Report + uses: actions/download-artifact@v4 + with: + name: test-report + path: . + + - name: Set up Production Environment run: | echo "PORT=${{ vars.PORT || 3000 }}" > .env echo "NODE_ENV=${{ vars.NODE_ENV || 'production' }}" >> .env @@ -27,9 +77,8 @@ jobs: echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env - - - name: Build and Restart with Docker Compose + - name: Build and Restart App Container run: | - docker-compose down - docker-compose up --build -d + docker compose down + docker compose up --build -d docker system prune -f diff --git a/.github/workflows/sync-main.yml b/.github/workflows/sync-main.yml index d5cb8ac..cfd2a66 100644 --- a/.github/workflows/sync-main.yml +++ b/.github/workflows/sync-main.yml @@ -5,11 +5,14 @@ on: branches: [ "production" ] workflow_dispatch: +permissions: + contents: write + pull-requests: write + jobs: sync-to-main: runs-on: ubuntu-latest # ONLY run if the push to production did NOT come from main - # This avoids the "Main -> Prod -> Main" circular loop if: github.event.head_commit.author.name != 'github-actions' && !contains(github.event.head_commit.message, 'main') steps: @@ -18,15 +21,25 @@ jobs: fetch-depth: 0 token: ${{ secrets.PR_TOKEN || secrets.GITHUB_TOKEN }} - - name: Sync changes to main + - name: Create PR to sync production back to main + env: + GH_TOKEN: ${{ secrets.PR_TOKEN || secrets.GITHUB_TOKEN }} run: | - git config --global user.name 'github-actions[bot]' - git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git fetch origin main production - git checkout main - git fetch origin production + # Check if there are differences + if git diff --quiet origin/main..origin/production; then + echo "No differences between production and main. Skipping." + exit 0 + fi - # Use allow-unrelated-histories to bridge the gap between branches if they diverged - git merge origin/production --allow-unrelated-histories -m "🤖 Sync hotfix from production to main" + # Create a sync branch from production + git checkout -b sync/prod-to-main origin/production - git push origin main + # Create PR using GitHub CLI + gh pr create \ + --base main \ + --head sync/prod-to-main \ + --title "🤖 Sync: Production hotfixes back to main" \ + --body "Automated PR to sync hotfix changes from production back to main." \ + || echo "PR already exists or nothing to sync." diff --git a/Dockerfile b/Dockerfile index 9d3625c..e41de39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,15 +33,12 @@ RUN npm install --omit=dev # Copy compiled backend COPY --from=build /app/dist ./dist -# Copy test report (generated during build test step if added to workflow, or copied if exists) -COPY --from=build /app/test-report.html ./test-report.html +# Copy test report (generated by GitHub Actions Host Runner) +COPY ./test-report.html ./test-report.html # Copy compiled frontend COPY --from=build /app/client/dist ./client/dist -# Copy test report (if generated) -COPY --from=build /app/test-report.html ./test-report.html - # Copy other necessary files (like uploads or assets) COPY src/uploads ./src/uploads diff --git a/ai_test_err.txt b/ai_test_err.txt new file mode 100644 index 0000000000000000000000000000000000000000..7622c0922f6a18f05733f7530d3a9d3ecbe092da GIT binary patch literal 15460 zcmeI3TTk3Z6vvO0w@Up6eegq55w$FLXn3h8frMNr0V=gB)n>aaWK}TR^^!mz`X%z2 zs(z-twEg{OnAl$I?Gi!~RU^yZ*}0tC%=~?9pYzY(i6z#y6H6`QdSo4Ia6Pd@`_w+6 z%^w@IcWuDgqP1(kbEfo3Y{fRXW{m6G3u@bx5_?H$V9zOiXFY0C+lJ;Tb?PU|Me5?w zqAjuev>!X#UE6Ve1I@-oZBd5K1@{T~_u)J>Bu7TgVS!?EsJ#Kt%$9A|7Tk5l?!1vL zMc#eJHaO2X6M7`J?l?ZC6gWzw%+Z-bhc5j?ji)X>SDbFEP}sBYFJ#CH`X!s^x_%52Xq1M8F%Z!w?Us9*z3(n7NlNr2cYfxXaA2@y87Uia`BIgFb51A{~ zi^?Iii&Xxww>^q0L^7>9>vZ670-tj@r`YC%mdl6cYLm7Bl4>n9$x2kurahwe$BX?C?7PKU*s)#4__eNU>ZCpg5jrL9H0HD0ueoZqYVRL8Ey^Rb z?-MsOqLaFLk`)psqp;$f@&v0jLDOtVo;n@}(CRuW0f#*H6sZRL1l_7+E!#@63cs<-MG9PRcT!G-iIBd|s@BwVt(;+x+(k&|p>i87<#I{Jn32-rn<($DxvtTdR4>{g>CL+F$q|e4w^N zrye!llN{#fy{rX%kR^7!ycF+e1zGhwN?uXT?{qgU=OX@;o4!k#6+eA|O&kx`=cilZSWJ=e~$|(YG7VKaPkOeLMeF{eAg%<)`fM{9AsaE&Jo6 z-!?_8i&z)2&cCfF@0<7>yUf$H-cEd|=h*5u({=Ch-tHZD#J`At5&t6omER18H;V5I z|H2nGp;7hMi~6r@b$B@Z)kj}ui(l3v@pb>D1!BrAWdt%|#j9$ECrqDK}zvT;4KkcpBIrLL?m zWql~;F(E>`oJXMSHPsD>wjc1Mc;xa5!gxQX7_mP?oOcxk)s|`O@cyjDw*((`;F;EFv@8Sc@1HN_FGkMr_a1t$6m_pQlz@i8`3kjr}U5-MYbz%FCnK* zk<)qJt@3enNA?<=G9rwZ9?!@}nshxRkMcM|PHl*l_xTympeVa1rM}WXgQIfm5?X!C zf;0~KU>c=Zds#;O6mzeP#RQ2?8K>xcuT6`*6Um@VsbQ-slTew9>aBT}F5ftM{{nO~ B=H&nY literal 0 HcmV?d00001 diff --git a/all_tests_out.txt b/all_tests_out.txt new file mode 100644 index 0000000000000000000000000000000000000000..776dd5e2d05b2238e5b292455525e41e5d95c302 GIT binary patch literal 95578 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 literal 0 HcmV?d00001 diff --git a/client/src/components/Avatar.tsx b/client/src/components/Avatar.tsx new file mode 100644 index 0000000..d82ff5e --- /dev/null +++ b/client/src/components/Avatar.tsx @@ -0,0 +1,81 @@ +import { useState, useEffect } from 'react'; +import { Image } from 'react-bootstrap'; +import { User } from 'lucide-react'; +import { getImageUrl } from '../services/api'; + +interface AvatarProps { + src?: string | null; + username?: string; + size?: number; + className?: string; + border?: boolean; +} + +const Avatar = ({ src, username, size = 40, className = '', border = false }: AvatarProps) => { + const [error, setError] = useState(false); + + // Reset error when source changes + useEffect(() => { + setError(false); + }, [src]); + + // Reset error if src changes + const hasImage = src && + src !== 'null' && + src !== 'undefined' && + src !== '' && + !src.includes('undefined') && + !src.includes('null'); + + if (hasImage && !error) { + return ( +
+ setError(true)} + alt={username || 'User avatar'} + /> +
+ ); + } + + // Fallback: Initials or Icon with premium dynamic colors + const initials = username ? username[0].toUpperCase() : null; + + // Generate a consistent color based on username + const colors = [ + { bg: 'rgba(13, 110, 253, 0.1)', text: '#0d6efd' }, // Blue + { bg: 'rgba(102, 16, 242, 0.1)', text: '#6610f2' }, // Indigo + { bg: 'rgba(111, 66, 193, 0.1)', text: '#6f42c1' }, // Purple + { bg: 'rgba(214, 51, 132, 0.1)', text: '#d63384' }, // Pink + { bg: 'rgba(253, 126, 20, 0.1)', text: '#fd7e14' }, // Orange + { bg: 'rgba(25, 135, 84, 0.1)', text: '#198754' } // Green + ]; + + const colorIndex = username ? username.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % colors.length : 0; + const selectedColor = colors[colorIndex]; + + return ( +
+ {initials ? initials : } +
+ ); +}; + +export default Avatar; diff --git a/client/src/components/CommentSection.tsx b/client/src/components/CommentSection.tsx index 4e2af8e..baf6d82 100644 --- a/client/src/components/CommentSection.tsx +++ b/client/src/components/CommentSection.tsx @@ -1,9 +1,9 @@ import { useState, useEffect, type FormEvent } from 'react'; -import { Card, Button, Form, Image, Spinner, Dropdown } from 'react-bootstrap'; +import { Card, Button, Form, Spinner, Dropdown } from 'react-bootstrap'; import { useAuth } from '../context/AuthContext'; import { getComments, createComment, deleteComment, updateComment, type Comment } from '../services/commentService'; import { Send, MoreVertical, Trash2, Edit2, X, Check } from 'lucide-react'; -import { getImageUrl } from '../services/api'; +import Avatar from './Avatar'; interface CommentSectionProps { postId: string; @@ -104,20 +104,7 @@ const CommentSection = ({ postId }: CommentSectionProps) => {
- {user?.image ? ( - - ) : ( -
- {user?.username?.[0] || 'U'} -
- )} + {
{comments.map(comment => (
- {comment.owner?.image ? ( - - ) : ( -
- {comment.owner?.username?.[0]?.toUpperCase() || 'U'} -
- )} +
diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index 36da468..5632fb1 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -1,38 +1,9 @@ -import { useState } from 'react'; import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; -import { Container, Navbar, Nav, Dropdown, Image } from 'react-bootstrap'; -import { LogOut, Search, PlusCircle, Home, TrendingUp, User, Bell } from 'lucide-react'; -import { getImageUrl } from '../services/api'; +import { Container, Navbar, Nav, Dropdown } from 'react-bootstrap'; +import { Bell, Home, LogOut, PlusCircle, Search, TrendingUp } from 'lucide-react'; -const ProfileAvatar = ({ user }: { user: any }) => { - const [imageError, setImageError] = useState(false); - - // Check if the image string is valid - const hasValidImageStr = user?.image && user.image !== 'null' && user.image !== 'undefined'; - - if (hasValidImageStr && !imageError) { - return ( - setImageError(true)} - /> - ); - } - - // Fallback UI - return ( -
- -
- ); -}; +import Avatar from './Avatar'; const Layout = () => { const { user, logout } = useAuth(); @@ -85,18 +56,23 @@ const Layout = () => { )} -