A Vue 3 productivity + portfolio web app combining task management, habit tracking, statistics, and a personal portfolio showcase.
Backend code located at https://github.com/philwing100/portfolio-backend
| Layer | Technology |
|---|---|
| Framework | Vue 3 + Vue Router 4 + Vuex 4 |
| UI Library | Vuetify 3 (Material Design) |
| HTTP | Axios (with auth interceptors) |
| Icons | FontAwesome 6 |
| Drag-and-drop | vuedraggable |
| Build | Webpack 5 via Vue CLI 5 |
| Other | js-cookie, lodash, vue-color |
src/
views/ # Page-level components
components/ # Feature-grouped reusable components
CalendarComponents/
ListItems/
HabitComponents/
LearnComponents/ (Pomodoro, PiP)
StatsComponents/
AboutMeComponents/
SettingsComponents/
SidebarComponents/
GeneralComponents/
router/
index.js # Router setup + auth guard
dashboardRoutes.js # / and /about-me
accountRoutes.js # /login and /settings
generalRoutes.js # /Streaks, /Stats, /projects/:slug
store.js # Vuex store (user, token, isAuthenticated)
api.js # API helper functions
axios.js # Axios instance with JWT interceptor
date.js # Date utility helpers
main.js # App entry point
| Path | View | Auth Required |
|---|---|---|
/ |
Dashboard | No |
/login |
Login | No |
/about-me |
AboutMe | No |
/Streaks |
Habits | No |
/Stats |
Stats | No |
/settings |
Settings | No |
/projects/:slug |
ProjectDetail | No |
/study |
Study (home) | No |
/study/folder/:id |
Study (folder drill-down) | No |
/study/session |
StudySession | No |
Auth guard is in router/index.js beforeEach. Routes with meta.requiresAuth: true redirect to /login.
src/store.js
state: { user, isAuthenticated, token }
SET_USER/LOGOUT/SET_TOKEN— mutationscheckAuth()— hits/auth/check-authto validate tokenhandleGoogleLogin()— extracts JWT from URL params after OAuth redirectlogout()— calls/auth/logout, clears state + localStorage
Token is persisted in localStorage('token') and injected by axios interceptor on every request.
Base URL:
- Production:
https://portfolio-backend-pi-liart.vercel.app - Local:
http://localhost:3000 - All calls go through
/apiprefix
src/api.js key functions:
createList(listData)— POST/lists/getList(identifier)— POST/lists/(filter byparent_page+date)getStreaks()— POST/streaks/updateStreak()— POST/streaks/axiosGet(url, params)/axiosPost(url, action, data)— generic wrappers
List payload shape: { parent_page, date, lists: [{ title, visible, color, items: [...] }] }
Item fields: textString, scheduledDate, scheduledTime, scheduledStartTime, scheduledEndTime, taskTimeEstimate, recurringTask, recurringFrequency, dueDateCheckbox, dueDate, complete
src/api/flashcards.js — Flashcard REST client (proper REST, not action-based):
flashcardApi.getSets()— GET/flashcards/setsflashcardApi.getSet(id)— GET/flashcards/sets/:id(includes all cards + SM-2 state)flashcardApi.createSet(set)/updateSet(id, set)/deleteSet(id)flashcardApi.addCards(setId, cards)/updateCard(id, card)/deleteCard(id)flashcardApi.reviewCard(cardId, rating)— POST/flashcards/cards/:id/reviewnormalizeSet(backendSet)/normalizeCard(c, setId)/normalizeAnkiResponse(d)— shape adapters- Folders have no backend support;
folderId+optionsare stored as JSON in the set'sdescriptionfield - Rating map: frontend 0-3 → backend grades
[1,3,4,5](Again=1/fail, Hard=3, Good=4, Easy=5)
Missing backend routes (needed for full feature parity):
GET /flashcards/setsshould includedue_countper set (avoid N+1 card fetches for counts)GET /flashcards/studyshould includeset_idper card (needed to route review updates)- Folder CRUD endpoints (
/flashcards/folders) — currently handled client-side via localStorage
- User hits
/login→ clicks "Sign in with Google" - Redirects to
{baseURL}/auth/google(Google OAuth) - Backend redirects back with
?token=...in URL handleGoogleLogin()action stores token → localStorage + Vuex- Axios interceptor adds
authorization: <token>to all requests
CSS custom properties on :root: --primaryColor, --secondaryColor, --accentColor.
Persisted in localStorage('siteColors') as JSON. Applied on app mount.
Edited via the Settings page with vue-color color pickers.
| Key | Contents |
|---|---|
token |
JWT string |
siteColors |
{ primaryColor, secondaryColor, accentColor } |
lifecalendar-birth-date |
Birth date for Stats page |
pomodoro_state |
Timer state (duration, mode, timestamps) |
dashboard-list-object:YYYY-MM-DD |
Cached task list per day |
study-data |
{ folders, sets } — all flashcard data |
File: src/views/Dashboard.vue
The primary productivity page. Shows a 24-hour daily calendar alongside a task list for the selected date.
Key components:
DailyCalendar.vue— 24-hour timeline with draggable events, current-time indicator (red line), pixel-based time slot renderingListElement.vue— Task list with checkboxes, inline editing, drag-and-drop reorderingPomodoro.vue— Work/break timer (50min work / 10min break default), circular SVG progress ring, supports PiPDateInput.vue— Date picker to navigate between days
Data flow: On mount, calls getList({ parent_page: 'dashboard', date: today }). Data saved back to backend on mutations. Cached in localStorage per date.
Mobile: activeMobileView toggle switches between calendar and list (not shown simultaneously on mobile).
File: src/views/Login.vue + src/components/Login.vue
Simple Google OAuth entry point. Redirects to the backend Google OAuth URL. On return, handleGoogleLogin() parses the token from the URL and stores it.
No form — just a "Sign in with Google" button.
File: src/views/Habits.vue
Daily and weekly habit tracker with streak counting.
Key components:
Habit.vue— Single habit card showing name, streak count, goal progress, and completion toggleHabitModal.vue— Create/edit/delete modal for a habit (name, frequency, goal)AddButton.vue— Floating button to open create modalDaysOfTheWeek.vue— Day selector for weekly habits
Data flow: Loads from getStreaks() on mount. Updates sent via updateStreak().
File: src/views/Stats.vue
Life calendar visualization: a 91-row × 52-column grid where each cell = one week of a ~91-year lifespan. Weeks lived are filled in; remaining weeks are empty.
Key components:
LifeCalendar.vue— Renders the grid, calculates weeks elapsed since birth dateDateInput.vue— Set birth date (persisted in localStorage)
The visual gives a memento-mori perspective on time.
File: src/views/Settings.vue
Theme color customization page.
Key components:
ColorPicker.vue— Wrapsvue-colorfor primary, secondary, and accent color selectionButton.vue— Save/apply
Changes update CSS variables on :root immediately and are persisted to localStorage('siteColors').
File: src/views/AboutMe.vue
Personal portfolio showcase page.
Key components:
FullPage.vue— Full-viewport-height scroll container with section navigationProfilePicture.vue— Hero image/introSideScrolling.vue— Horizontally scrollable skills sectionProjects.vue— Featured project cards/carouselExperience.vue— Work experience timelineConnectWithMe.vue— Social/contact links
Static content — no API calls. All data is hardcoded in components.
Files: src/views/Study.vue, src/router/studyRoutes.js
Home page for the ANKI-style flashcard system. Shows folders and sets in a responsive grid. When a folderId prop is present (route /study/folder/:id), drills into that folder's sets.
Key components:
FolderCard.vue— Clickable folder tile with color bar and context-menu (edit/delete)SetCard.vue— Clickable set tile showing card count, due badge, new badge; context-menu (edit/delete)FolderModal.vue— Create/edit folder (name + color)SetModal.vue— Create/edit set; three tabs: Cards (front/back pairs), Options (newPerDay, order, folder), Import/Export (bulk text with custom delimiters)
Data flow:
- On mount: shows
localStorage('study-data')immediately, then fetches from backend if authenticated (GET all sets, then GET each set in parallel for full card + SM-2 data) - Unauthenticated or backend error: falls back to localStorage. Default demo data shown when localStorage is empty.
- Folders have no backend; stored in localStorage only.
folderIdis serialized into each set's backenddescriptionfield as JSON. - "ANKI All" button counts all due cards across the visible scope; launches a session with only due cards (
buildSession(sets, 'due')) - Clicking a set starts a full study session — due + new cards up to
newPerDay(buildSession([set], 'study')) - Before navigating, populates
studySession.jssingleton withcardsand an asynconRate(cardId, setId, rating)callback onRate(authenticated): callsflashcardApi.reviewCard→ gets back updated SM-2 state → applies to local cardonRate(unauthenticated): applies local SM-2 viareviewCard()fromanki.js- Set CRUD: create → POST set + batch POST cards; update → PUT set + diff cards (delete removed, PUT existing, POST new); delete → DELETE set (backend cascades to cards)
ANKI algorithm: src/anki.js — SM-2 (easeFactor, interval, repetitions, nextReview). Ratings: 0=Again, 1=Hard, 2=Good, 3=Easy. Backend SM-2 is authoritative when authenticated (frontend grades map to backend 0-5 scale via [1,3,4,5]).
File: src/views/StudySession.vue
Active flashcard review session.
Key components:
FlashcardDisplay.vue— 3-D CSS flip card; click front to reveal back, emitsfliponce per card
Flow:
- Reads cards from
studySession.jssingleton; redirects to/studyif empty - Shows one card at a time with a progress bar
- After flip: four rating buttons appear (Again / Hard / Good / Easy), each color-coded
- On rating: calls
session.onRateto persist ANKI state, advances to next card - When all cards done: summary screen with per-rating counts and "Back to Study" button
File: src/views/ProjectDetail.vue
Detailed view for a specific portfolio project (e.g., GPU K-Means clustering). Content is hardcoded per slug in the component. No API calls.
src/date.js:
getTodayDate()→YYYY-MM-DDincrementDate(str)/decrementDate(str)→ shift by 1 daynormalizeDate(datetimeStr)→ MySQL DATETIME →YYYY-MM-DD
Pomodoro PiP:
Uses DocumentPictureInPicture browser API to float the timer in its own window. State synced via BroadcastChannel across tabs.
Sidebar:
src/components/SidebarComponents/SideBar.vue — Collapsible nav. Width toggles: 0 / 11.25rem (desktop), 16rem (mobile). Controlled by isMobile + toggle state.
src/anki.js:
RATINGS—[{ value, label, color }]for Again/Hard/Good/EasyinitAnki()— default SM-2 state for a new cardreviewCard(ankiState, rating)→ updated state with neweaseFactor,interval,repetitions,nextReviewisDue(card)/isNew(card)— card state predicatesgetDueCount(cards)— count of due cards in an arraybuildSession(sets, mode)— returns shuffled flat card array;mode='due'(ANKI All) ormode='study'(due + new up tonewPerDay)
src/studySession.js:
Singleton that bridges Study.vue → StudySession.vue. Study.vue sets session.cards, session.title, and session.onRate before navigating; StudySession.vue reads and calls them.
src/components/UI.md:
Quick-reference for every reusable component — props, emits, and usage snippets.