From e2802722bdc69a449de2c03d4198769730b5e83f Mon Sep 17 00:00:00 2001 From: existentialcoder Date: Sun, 23 Aug 2026 15:49:04 +0200 Subject: [PATCH] chore: move all svg components and data calls to stores --- apps/backend/Makefile | 1 + ...24_add_logo_url_to_skills_and_companies.py | 27 +- apps/backend/src/api/deps/plan.py | 13 +- apps/backend/src/api/v1/routes/users.py | 47 +- apps/backend/src/schemas/settings.py | 40 ++ apps/backend/src/schemas/user.py | 15 +- apps/backend/src/services/user.py | 10 +- apps/browser-extension/public/icon-128.svg | 11 - apps/browser-extension/public/icon-48.svg | 11 - apps/browser-extension/public/icon-64.svg | 11 - apps/browser-extension/public/icon.svg | 11 - apps/browser-extension/src/popup.vue | 109 +-- apps/frontend/package.json | 1 - .../public/logo-concepts/final-logo.svg | 103 --- apps/frontend/public/rafiki-clean.svg | 1 - apps/frontend/public/rafiki-inner.svg | 1 - apps/frontend/public/rafiki-original.svg | 1 - apps/frontend/public/rafiki-processed.svg | 6 - apps/frontend/public/rafiki-recolored.svg | 1 - apps/frontend/public/wordmark.svg | 13 - apps/frontend/src/App.vue | 14 +- .../src/assets/images/auth-screen-side.svg | 594 +++++++++++++++++ apps/frontend/src/assets/images/logo.svg | 0 .../applications/BoardApplications.vue | 36 +- .../applications/BoardSettingsModal.vue | 40 +- .../applications/JobDetailPanel.vue | 346 ++-------- .../applications/JobFiltersPanel.vue | 19 +- .../applications/TableApplications.vue | 18 +- apps/frontend/src/components/core/AppLogo.vue | 43 +- .../src/components/core/AppNavbar.vue | 22 +- .../src/components/core/AuthSidebarArt.vue | 596 +++++++++++++++++ .../src/components/core/AuthSplitLayout.vue | 626 +----------------- .../components/core/sidebar/AppSidebar.vue | 6 +- .../components/core/sidebar/SidebarInner.vue | 30 +- apps/frontend/src/lib/dataservice.ts | 135 ++-- apps/frontend/src/main.ts | 2 - apps/frontend/src/stores/app.ts | 150 +---- apps/frontend/src/stores/auth.ts | 13 +- apps/frontend/src/stores/boards.ts | 96 +++ apps/frontend/src/stores/companies.ts | 4 +- apps/frontend/src/stores/features.ts | 11 +- apps/frontend/src/stores/resumes.ts | 68 ++ apps/frontend/src/stores/settings.ts | 238 +++++++ apps/frontend/src/stores/skills.ts | 52 ++ apps/frontend/src/views/AllApplications.vue | 7 +- apps/frontend/src/views/Applications.vue | 85 +-- apps/frontend/src/views/AuthCallback.vue | 10 +- apps/frontend/src/views/AuthRelay.vue | 10 +- apps/frontend/src/views/BoardDetail.vue | 69 +- apps/frontend/src/views/Boards.vue | 129 +--- apps/frontend/src/views/Home.vue | 98 +-- apps/frontend/src/views/Login.vue | 59 +- apps/frontend/src/views/Plugins.vue | 74 +-- apps/frontend/src/views/Profile.vue | 142 +--- apps/frontend/src/views/ResetPassword.vue | 318 +++++---- apps/frontend/src/views/Resumes.vue | 244 ++----- apps/frontend/src/views/Signup.vue | 59 +- apps/website/components/DewLogo.vue | 29 - apps/website/components/ExtensionSection.vue | 2 + ...ExtensionSection.vue.tmp.6179.bf51f1c2228f | 84 --- apps/website/components/TheFooter.vue | 5 +- apps/website/components/TheNav.vue | 5 +- apps/website/nuxt.config.ts | 6 + docker/docker-compose.yml | 8 +- packages/ui/src/components/icons/AtsGauge.vue | 62 ++ packages/ui/src/components/icons/DewLogo.vue | 45 ++ .../ui/src/components/icons/DonutChart.vue | 54 ++ .../ui/src/components/icons/GithubLogo.vue | 13 + .../ui/src/components/icons/GmailLogo.vue | 18 + .../components/icons/GoogleCalendarLogo.vue | 15 + .../ui/src/components/icons/GoogleLogo.vue | 16 + .../ui/src/components/icons/LinkedInLogo.vue | 13 + packages/ui/src/components/icons/index.ts | 8 + packages/ui/src/index.ts | 3 + packages/ui/src/theme/index.ts | 1 - yarn.lock | 5 - 76 files changed, 2635 insertions(+), 2653 deletions(-) create mode 100644 apps/backend/src/schemas/settings.py delete mode 100644 apps/browser-extension/public/icon-128.svg delete mode 100644 apps/browser-extension/public/icon-48.svg delete mode 100644 apps/browser-extension/public/icon-64.svg delete mode 100644 apps/browser-extension/public/icon.svg delete mode 100644 apps/frontend/public/logo-concepts/final-logo.svg delete mode 100644 apps/frontend/public/rafiki-clean.svg delete mode 100644 apps/frontend/public/rafiki-inner.svg delete mode 100644 apps/frontend/public/rafiki-original.svg delete mode 100644 apps/frontend/public/rafiki-processed.svg delete mode 100644 apps/frontend/public/rafiki-recolored.svg delete mode 100644 apps/frontend/public/wordmark.svg create mode 100644 apps/frontend/src/assets/images/auth-screen-side.svg create mode 100644 apps/frontend/src/assets/images/logo.svg create mode 100644 apps/frontend/src/components/core/AuthSidebarArt.vue create mode 100644 apps/frontend/src/stores/boards.ts create mode 100644 apps/frontend/src/stores/resumes.ts create mode 100644 apps/frontend/src/stores/settings.ts create mode 100644 apps/frontend/src/stores/skills.ts delete mode 100644 apps/website/components/DewLogo.vue delete mode 100644 apps/website/components/ExtensionSection.vue.tmp.6179.bf51f1c2228f create mode 100644 packages/ui/src/components/icons/AtsGauge.vue create mode 100644 packages/ui/src/components/icons/DewLogo.vue create mode 100644 packages/ui/src/components/icons/DonutChart.vue create mode 100644 packages/ui/src/components/icons/GithubLogo.vue create mode 100644 packages/ui/src/components/icons/GmailLogo.vue create mode 100644 packages/ui/src/components/icons/GoogleCalendarLogo.vue create mode 100644 packages/ui/src/components/icons/GoogleLogo.vue create mode 100644 packages/ui/src/components/icons/LinkedInLogo.vue create mode 100644 packages/ui/src/components/icons/index.ts diff --git a/apps/backend/Makefile b/apps/backend/Makefile index ede32e3..c6621b0 100644 --- a/apps/backend/Makefile +++ b/apps/backend/Makefile @@ -35,6 +35,7 @@ run-db: docker run \ --name $(DB_CONTAINER) \ -e POSTGRES_PASSWORD=password \ + -e POSTGRES_DB=job_applica \ -p 5432:5432 \ -v $(DB_VOLUME):/var/lib/postgresql/data \ -d postgres:16; \ diff --git a/apps/backend/alembic/versions/ea3fb2d04124_add_logo_url_to_skills_and_companies.py b/apps/backend/alembic/versions/ea3fb2d04124_add_logo_url_to_skills_and_companies.py index 7e53fb9..66a8410 100644 --- a/apps/backend/alembic/versions/ea3fb2d04124_add_logo_url_to_skills_and_companies.py +++ b/apps/backend/alembic/versions/ea3fb2d04124_add_logo_url_to_skills_and_companies.py @@ -5,25 +5,32 @@ Create Date: 2025-11-23 17:32:55.858487 """ -from typing import Sequence, Union -from alembic import op -import sqlalchemy as sa +from collections.abc import Sequence +import sqlalchemy as sa +from alembic import op +from sqlalchemy import inspect # revision identifiers, used by Alembic. revision: str = 'ea3fb2d04124' -down_revision: Union[str, Sequence[str], None] = '0000000000' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | Sequence[str] | None = '0000000000' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('companies', sa.Column('logo_url', sa.String(length=500), nullable=True)) - op.add_column('skills', sa.Column('logo_url', sa.String(length=500), nullable=True)) - # ### end Alembic commands ### + conn = op.get_bind() + insp = inspect(conn) + + companies_cols = {c['name'] for c in insp.get_columns('companies')} + skills_cols = {c['name'] for c in insp.get_columns('skills')} + + if 'logo_url' not in companies_cols: + op.add_column('companies', sa.Column('logo_url', sa.String(length=500), nullable=True)) + if 'logo_url' not in skills_cols: + op.add_column('skills', sa.Column('logo_url', sa.String(length=500), nullable=True)) def downgrade() -> None: diff --git a/apps/backend/src/api/deps/plan.py b/apps/backend/src/api/deps/plan.py index 2b9290b..ec0ce0d 100644 --- a/apps/backend/src/api/deps/plan.py +++ b/apps/backend/src/api/deps/plan.py @@ -7,6 +7,7 @@ from ...core.config import settings from ...schemas.user import UserBase from ...services import plan as plan_service +from ...services import user as user_service from .auth import get_current_user from .db import get_db @@ -15,12 +16,13 @@ def plan_gate(resource: str, count_fn: Callable[[int], Any]): """Dependency factory that enforces a plan resource limit and injects the warning header.""" async def dep( + response: Response, user: UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), - response: Response = None, ): if not settings.CHECK_PLAN_LIMIT: return + assert user.id is not None # populated for any authenticated user warning = await plan_service.check_plan_limit(db, user.id, user.plan, resource, count_fn(user.id)) response.headers.update(plan_service.warning_header(warning)) @@ -30,9 +32,14 @@ async def dep( def extraction_gate(): """Dependency that enforces the monthly extraction quota.""" - async def dep(user: UserBase = Depends(get_current_user)): + async def dep( + user: UserBase = Depends(get_current_user), + db: AsyncSession = Depends(get_db), + ): if not settings.CHECK_PLAN_LIMIT: return - plan_service.check_extraction_limit(user.plan, user.settings) + assert user.id is not None # populated for any authenticated user + user_settings = await user_service.get_settings(db, user.id) + plan_service.check_extraction_limit(user.plan, user_settings) return Depends(dep) diff --git a/apps/backend/src/api/v1/routes/users.py b/apps/backend/src/api/v1/routes/users.py index 71ce6f1..cadf2f9 100644 --- a/apps/backend/src/api/v1/routes/users.py +++ b/apps/backend/src/api/v1/routes/users.py @@ -5,7 +5,7 @@ from ....core.config import settings from ....models.resume import Resume -from ....schemas import user as schemas +from ....schemas import user as user_schemas from ....services import resume as resume_service from ....services import user as user_service from ...deps.auth import get_current_user @@ -27,7 +27,7 @@ class ChangePasswordRequest(BaseModel): new_password: str -def _check_self(user_id: int, current_user: schemas.UserBase): +def _check_self(user_id: int, current_user: user_schemas.UserBase): if user_id != current_user.id: raise HTTPException(status_code=403, detail='Forbidden') @@ -47,9 +47,9 @@ def _resume_response(r, user_id: int) -> dict: } -@public_router.post('/check-user-name', response_model=schemas.UserNameCheckResponse) +@public_router.post('/check-user-name', response_model=user_schemas.UserNameCheckResponse) async def check_user_name_availability( - payload: schemas.UserNameCheckRequest, + payload: user_schemas.UserNameCheckRequest, db: AsyncSession = Depends(get_db), ): return await user_service.check_user_name_availability(db, payload.user_name) @@ -57,13 +57,13 @@ async def check_user_name_availability( @public_router.get('/security-questions', response_model=list[str]) async def get_security_questions(): - return [q.value for q in schemas.SecurityQuestion] + return [q.value for q in user_schemas.SecurityQuestion] -@router.get('/{user_id}', response_model=schemas.UserBase) +@router.get('/{user_id}', response_model=user_schemas.UserBase) async def get_user( user_id: int, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), ): _check_self(user_id, current_user) return current_user @@ -73,7 +73,7 @@ async def get_user( async def update_user( user_id: int, payload: UpdateProfileRequest, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) @@ -90,7 +90,7 @@ async def update_user( async def upload_avatar( user_id: int, file: UploadFile = File(...), - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) @@ -102,7 +102,7 @@ async def upload_avatar( async def change_password( user_id: int, payload: ChangePasswordRequest, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) @@ -113,7 +113,7 @@ async def change_password( @router.get('/{user_id}/settings') async def get_settings( user_id: int, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) @@ -123,18 +123,19 @@ async def get_settings( @router.patch('/{user_id}/settings') async def update_settings( user_id: int, - payload: schemas.UserSettings, - current_user: schemas.UserBase = Depends(get_current_user), + payload: user_schemas.UserSettingsRequest, + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) - return {'settings': await user_service.update_settings(db, user_id, payload.settings)} + settings_patch = payload.settings.model_dump(exclude_unset=True) + return {'settings': await user_service.update_settings(db, user_id, settings_patch)} @router.get('/{user_id}/skills') async def get_user_skills( user_id: int, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) @@ -145,7 +146,7 @@ async def get_user_skills( async def add_user_skill( user_id: int, skill_id: int, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) @@ -156,7 +157,7 @@ async def add_user_skill( async def remove_user_skill( user_id: int, skill_id: int, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) @@ -166,7 +167,7 @@ async def remove_user_skill( @router.get('/{user_id}/resumes') async def list_resumes( user_id: int, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) @@ -180,14 +181,14 @@ async def list_resumes( ) async def upload_resume( user_id: int, + background_tasks: BackgroundTasks, file: UploadFile = File(...), - background_tasks: BackgroundTasks = None, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) resume = await resume_service.upload_resume(db, user_id, file) - if background_tasks and resume.parsed_text: + if resume.parsed_text: background_tasks.add_task(resume_service.sync_skills_background, user_id, resume.parsed_text) return _resume_response(resume, user_id) @@ -196,7 +197,7 @@ async def upload_resume( async def delete_resume( user_id: int, resume_id: int, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) @@ -208,7 +209,7 @@ async def delete_resume( async def set_default_resume( user_id: int, resume_id: int, - current_user: schemas.UserBase = Depends(get_current_user), + current_user: user_schemas.UserBase = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): _check_self(user_id, current_user) diff --git a/apps/backend/src/schemas/settings.py b/apps/backend/src/schemas/settings.py new file mode 100644 index 0000000..a83f356 --- /dev/null +++ b/apps/backend/src/schemas/settings.py @@ -0,0 +1,40 @@ +import enum +from typing import Any + +from pydantic import BaseModel + + +class UserTheme(enum.StrEnum): + LIGHT = 'light' + DARK = 'dark' + SYSTEM = 'system' + + +class ViewMode(enum.StrEnum): + BOARD = 'board' + LIST = 'list' + + +class DarkBgTheme(enum.StrEnum): + NOIR = 'noir' + MOCHA = 'mocha' + ABYSS = 'abyss' + MIDNIGHT = 'midnight' + + +class LightBgTheme(enum.StrEnum): + WHITE = 'white' + CANVAS = 'canvas' + ARCTIC = 'arctic' + BLUSH = 'blush' + + +class UserSettings(BaseModel): + theme: UserTheme = UserTheme.SYSTEM + view_mode: ViewMode = ViewMode.LIST + per_page: int = 10 + dark_bg_theme: DarkBgTheme = DarkBgTheme.NOIR + light_bg_theme: LightBgTheme = LightBgTheme.WHITE + sidebar_expanded: bool = True + hidden_widgets: list[str] = [] + saved_job_filters: dict[str, Any] | None = None diff --git a/apps/backend/src/schemas/user.py b/apps/backend/src/schemas/user.py index 6857cf5..d02659f 100644 --- a/apps/backend/src/schemas/user.py +++ b/apps/backend/src/schemas/user.py @@ -4,6 +4,7 @@ from pydantic import BaseModel, EmailStr, model_validator from .base import BaseSchema +from .settings import UserSettings class UserSignupKey(enum.StrEnum): @@ -29,7 +30,6 @@ class UserBase(BaseSchema): security_question: str | None = None avatar_url: str | None = None plan: str = 'free' - settings: dict[str, Any] = {} @model_validator(mode='before') @classmethod @@ -37,16 +37,14 @@ def _derive_has_password(cls, data: Any) -> Any: hashed_password = ( data.get('hashed_password') if isinstance(data, dict) else getattr(data, 'hashed_password', None) ) - has_password = bool(hashed_password) and len(hashed_password) > 0 + has_password = bool(hashed_password) if isinstance(data, dict): return {**data, 'has_password': has_password} return {**{field: getattr(data, field, None) for field in cls.model_fields}, 'has_password': has_password} -class UserSettings(BaseModel): - """Partial update payload — only provided keys are merged into existing settings.""" - - settings: dict[str, Any] +class UserSettingsRequest(BaseModel): + settings: UserSettings class UserSignup(BaseModel): @@ -66,11 +64,6 @@ def _require_security_question_without_email(self): return self -class UserLogin(BaseModel): - user_id: str - password: str - - class UserLoginTokenResponse(BaseModel): message: str access_token: str diff --git a/apps/backend/src/services/user.py b/apps/backend/src/services/user.py index 4d8b645..1b602a4 100644 --- a/apps/backend/src/services/user.py +++ b/apps/backend/src/services/user.py @@ -2,6 +2,7 @@ import jwt from fastapi import HTTPException, UploadFile +from fastapi.security import OAuth2PasswordRequestForm from jwt import InvalidTokenError from pydantic import EmailStr, ValidationError from sqlalchemy import func, select @@ -12,7 +13,8 @@ from ..core.utils import create_token, hash_password, verify_password from ..models.skill import Skill from ..models.user import User -from ..schemas.user import TokenPayload, UserBase, UserLogin, UserLoginTokenResponse, UserNameCheckResponse, UserSignup +from ..schemas.settings import UserSettings +from ..schemas.user import TokenPayload, UserBase, UserLoginTokenResponse, UserNameCheckResponse, UserSignup from ..utils.file_uploader import FileUploader UPLOAD_BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'uploads')) @@ -42,11 +44,13 @@ async def create_user(db: AsyncSession, user_data: UserSignup) -> UserBase: raise HTTPException(status_code=409, detail=f'User already exists with user_name: ${user_data.user_name}') user_name = user_data.email or user_data.user_name + assert user_name is not None # signup_user route already rejects payloads with neither user_obj = User(**user_data.model_dump(exclude={'password', 'security_answer'}, exclude_unset=True)) user_obj.hashed_password = hash_password(user_data.password) user_obj.user_name = user_name if user_data.security_answer: user_obj.hashed_security_answer = hash_password(user_data.security_answer) + user_obj.settings = UserSettings().model_dump() db.add(user_obj) await db.commit() @@ -60,7 +64,7 @@ async def check_user_name_availability(db: AsyncSession, user_name: str) -> User return UserNameCheckResponse(is_available=existing_user_count.scalar() == 0) -async def user_login(db: AsyncSession, login_data: UserLogin) -> UserLoginTokenResponse: +async def user_login(db: AsyncSession, login_data: OAuth2PasswordRequestForm) -> UserLoginTokenResponse: result = await db.execute( select(User).where((User.email == login_data.username) | (User.user_name == login_data.username)) ) @@ -77,7 +81,7 @@ async def user_login(db: AsyncSession, login_data: UserLogin) -> UserLoginTokenR if not verify_password(login_data.password, user_data.hashed_password): raise HTTPException(status_code=401, detail='Incorrect password') - token_data: TokenPayload = { + token_data: dict = { 'sub': str(user_data.id), 'user_name': user_data.user_name, 'signup_key': user_data.signup_key, diff --git a/apps/browser-extension/public/icon-128.svg b/apps/browser-extension/public/icon-128.svg deleted file mode 100644 index a14e666..0000000 --- a/apps/browser-extension/public/icon-128.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/apps/browser-extension/public/icon-48.svg b/apps/browser-extension/public/icon-48.svg deleted file mode 100644 index 9745f5c..0000000 --- a/apps/browser-extension/public/icon-48.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/apps/browser-extension/public/icon-64.svg b/apps/browser-extension/public/icon-64.svg deleted file mode 100644 index ea4292c..0000000 --- a/apps/browser-extension/public/icon-64.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/apps/browser-extension/public/icon.svg b/apps/browser-extension/public/icon.svg deleted file mode 100644 index 0ec42a2..0000000 --- a/apps/browser-extension/public/icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/apps/browser-extension/src/popup.vue b/apps/browser-extension/src/popup.vue index 2579a48..5cebd83 100644 --- a/apps/browser-extension/src/popup.vue +++ b/apps/browser-extension/src/popup.vue @@ -4,6 +4,7 @@ import dataservice, { type BoardData, type JobExtractResult, type CompanyResult, import ext from './lib/ext'; import { config, loadConfig, saveConfig, resetConfig } from './lib/config'; import { DEFAULT_COMPANY_LOGO_URL } from '@job-applica/ui/lib/constants'; +import { DewLogo, GoogleLogo, LinkedInLogo, AtsGauge, Icon } from '@job-applica/ui'; import { Button } from '@job-applica/ui/components/ui/button'; import { Input } from '@job-applica/ui/components/ui/input'; import { Label } from '@job-applica/ui/components/ui/label'; @@ -508,20 +509,7 @@ const platformBadgeVariant: Record = {