EES Survey Portal — Frontend Developer Documentation¶
Audience: Frontend developers only. Goal: A complete onboarding + reference guide so a new frontend developer can understand the project, navigate the code, and start contributing without a live walkthrough.
Ground rule: Everything here is derived from the actual source code at
d:/Downloads/EES Documentation/EES-2026/EES-Survey-Portal. Where something cannot be determined from the code, it says "Unable to determine from the available code." Nothing is invented.ℹ️ Scope note. This is the Survey Portal — the employee-facing, survey-taking web app for the EES (Employee Engagement Survey) product line. It is a separate product from the "EES Report" admin/analytics app, which is documented elsewhere and is not covered here. This app's only job is: receive an invite link → resolve a questionnaire → let one employee answer it in their preferred language → submit it. There is no admin UI, no authentication login screen, and no dashboard in this codebase.
Table of Contents¶
1. Introduction¶
Purpose of the frontend¶
The Survey Portal is the employee-facing survey-taking web application for the EES product line — a consultancy's employee-engagement-survey platform. An employee receives a unique invite link, opens it in a browser, picks a language (English or Urdu), reads a welcome/instructions screen, answers a sequence of Likert-scale / quiz-style engagement questions (plus at least one open-text "integrity" follow-up type), reviews all their answers on a single grid, and submits. The app is single-purpose: there is no login form, no account creation, and no navigation menu — it is a linear, wizard-style, single-respondent flow gated by a token embedded in the URL.
High-level overview¶
- One React single-page application, built with Vite, that renders entirely inside
#root(index.html:11). - All app state lives in a single Redux slice (
survey) — there is no per-page local data-fetching layer beyond that slice. - All backend communication goes through one Axios instance (
src/api/axiosInstance.js) hitting a single external host, and one service module (src/services/questionsService.js) with four methods. - The app is white-labeled per client: the current checkout is configured for "Dubai Islamic Bank" (see
src/Layout/index.jsx:91,index.html:7), butpublic/survey-main.blade.php(a leftover built-output snapshot) and asset filenames likesrc/assets/mdow.png/mdow-alt.png("Martin Dow") show the same codebase has been built and deployed for at least one other client. Branding copy (the welcome text) is pulled at runtime frompublic/branding.jsonrather than hardcoded in most places (src/pages/Start.jsx:16), though the bank name and page<title>are still hardcoded per build.
Package identity (observed fact)¶
package.json:2 sets "name": "Jubilee-survey" — an internal/client project codename retained in the manifest and also reused as the Redux slice's internal name (src/redux/slices/surveySlice.js:17, name: "Jubilee-survey"). This is almost certainly leftover naming from an earlier or sibling engagement and does not reflect the DIB/EES branding shown to end users. It is noted here as an observed fact, not corrected, since renaming it is out of scope for documentation.
Technology stack¶
| Concern | Choice | Version (from package.json) |
|---|---|---|
| Language | JavaScript (JSX) — not TypeScript | plain .jsx/.js; @types/react and @types/react-dom are present as dev dependencies but no tsconfig.json exists and no file uses TS syntax |
| Framework | React | ^18.3.1 |
| Build tooling | Vite | ^5.4.8, via @vitejs/plugin-react ^4.3.2 |
| State management | Redux Toolkit + React-Redux | @reduxjs/toolkit ^2.3.0, react-redux ^9.1.2 |
| Routing | React Router (hash router) | react-router-dom ^6.26.2 |
| Styling | Tailwind CSS v3 | tailwindcss ^3.4.13, plus postcss ^8.4.47, autoprefixer ^10.4.20 |
| Animation | Framer Motion | framer-motion ^11.17.0 |
| HTTP client | Axios | axios ^1.7.9 |
| Internationalization | i18next + react-i18next | i18next ^24.1.0, react-i18next ^15.2.0 |
| Alerts/dialogs | SweetAlert2 | sweetalert2 ^11.15.3 |
| Icons | react-icons | react-icons ^5.3.0 |
| Linting | ESLint 9 (flat config) | eslint ^9.11.1 + eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-react-refresh |
Design principles (as observed in the code)¶
- Single-slice global state. The entire application's data model — questions, answers, current position, language, status — lives in one Redux slice (
survey), read directly viauseSelector((state) => state.survey)throughout. There are no other slices, no RTK Query, no middleware beyond Redux Toolkit's defaults. - Link/token-driven bootstrap. The app does not "log in." A single
Layoutcomponent (src/Layout/index.jsx) reads aqstr_urltoken from the route (/:id) orsessionStorage, and on every mount fetches the full questionnaire + prior answers + status in one call before rendering any page. - Server-echoed progress. The current question index and completion state are not purely client-derived — they are reconstructed from the count of already-answered questions returned by the backend (
setDatareducer,src/redux/slices/surveySlice.js:30-77), so refreshing or reopening the link resumes where the employee left off. - Linear wizard UI. Pages/components correspond 1:1 to wizard steps (
Language→Start→quiz→Review→FeedbackSubmission→SurveyCompletion), navigated withreact-router-dom'suseNavigate, not a stepper/state machine library. - Per-question-type renderer switch.
quiz.jsxis a single large component that switches oncurrentQuestion.questionType(round_slider,nps_slider,box,square_slider,integrity) to pick which Likert/answer component to render — there is no generic "question renderer" abstraction. - Manual bilingual strings, not full i18next usage. Although
i18next/react-i18nextis initialized (src/i18n.js), most components do not use theuseTranslationhook; instead they importsrc/locales/en.json/ur.jsondirectly as plain objects and branch onlanguage === "eng"(see §12). - Hardcoded production API host.
src/api/axiosInstance.js:6bakes the production backend URL directly into the committed source rather than reading it from a Vite env variable — see §22.
2. Project Structure¶
EES-Survey-Portal/
├── public/ # Static assets served as-is + one legacy build artifact
│ ├── background.png, bg-new.png, logo*.png, logo-bg.jpg
│ ├── branding.json # Runtime-fetched welcome copy (en/ur) for Start.jsx
│ ├── smileyLikert/1-5.png # Images for the "box" (SmileyLikert) question type
│ ├── smileys/*.gif # Alternate smiley set (referenced by RoundedLikert/SquaredLikert/Slider, currently unused in markup)
│ └── survey-main.blade.php # A checked-in snapshot of a PRODUCTION BUILD's <head> for a different client ("Martin Dow") — evidence this app is deployed per-client
├── src/
│ ├── api/ # Axios instance, thin CRUD wrapper, error normalizer (see §9)
│ ├── assets/ # Logos, background art, one custom Urdu font (see §15)
│ ├── components/ # All reusable + survey-specific UI (see §7)
│ │ └── LikertScale/ # The three visual Likert-style answer widgets
│ ├── Layout/ # The single route-shell component + its CSS
│ ├── locales/ # en.json / ur.json — flat key→string maps (see §12)
│ ├── pages/ # The five wizard screens + barrel export (see §6)
│ ├── redux/ # Store, root reducer, the one "survey" slice (see §8)
│ ├── routes/ # Hash router config + the one route guard (see §4)
│ ├── services/ # questionsService.js — the 4-method API surface (see §9)
│ ├── App.jsx # Unused placeholder component (not rendered — see §3)
│ ├── main.jsx # React entry point: Provider + RouterProvider
│ ├── i18n.js # i18next bootstrap (loaded but underused — see §12)
│ └── index.css # Tailwind directives + global font-face + font-family
├── index.html # Vite entry HTML (title: "Dubai Islamic Bank Employee Engagement Survey 2026")
├── vite.config.js # Dev server host/port + React plugin only
├── tailwind.config.js # Custom "primary" color palette, Montserrat font family
├── postcss.config.js # tailwindcss + autoprefixer
├── eslint.config.js # Flat ESLint config (React + hooks + refresh)
└── package.json # name: "Jubilee-survey"
Folder-by-folder (why each exists)¶
| Folder/file | Purpose | Interacts with |
|---|---|---|
src/api/ |
The single HTTP boundary: base URL, timeout, bearer-token header injection, and a shared error-shaping function. | src/services/questionsService.js |
src/services/ |
One domain service (questionsService) wrapping the four survey-API endpoints. Pages/components call this, never axios/apiClient directly. |
src/api/apiClient.js, pages, Layout |
src/redux/ |
The entire client-side data model: store.js (store setup), rootReducer.js (combines one reducer), slices/surveySlice.js (state + all reducers/actions). |
Every page and most components (via useSelector/useDispatch) |
src/routes/ |
routes.jsx builds the createHashRouter tree; StatusRoute.jsx is a one-purpose guard that redirects already-completed surveys to /completion. |
Layout, pages, SurveyCompletion, FeedbackSubmission |
src/Layout/ |
The route-tree root element (rendered for every path). Owns the "fetch questionnaire on load" side effect, the full-page loading spinner, and the persistent chrome (bank name header, footer credit). | questionsService, redux/slices/surveySlice, react-router-dom's <Outlet/> |
src/pages/ |
The four page-level screens reachable as routes (Language, Start, quiz as QuizScreen, Review). Two more "page-like" screens (SurveyCompletion, FeedbackSubmission) live under components/ instead but are routed the same way — see §6. |
components/, redux/, services/ |
src/components/ |
Everything else: generic UI primitives (Button, Modal, DropDown, Accordion, ProgressBar, checkbox/radio selectors) and survey-specific widgets (the three LikertScale/* variants, Demographics, QuizTimer, FeedbackSubmission, SurveyCompletion). |
Pages, redux/, services/ |
src/locales/ |
Flat JSON dictionaries (en.json, ur.json) with ~16 UI-chrome strings (buttons, review/submission copy). Imported directly as JS objects in most consumers rather than through i18next's t(). |
quiz.jsx, Review.jsx, FeedbackSubmission.jsx, SurveyCompletion.jsx |
src/assets/ |
Logos (bank + prior client), a background illustration, and a bundled Urdu font file used for RTL question/answer text. | index.css (@font-face), Layout, pages |
src/App.jsx |
A default Vite scaffold component (<div>App</div>). Not imported or rendered anywhere — main.jsx renders <RouterProvider> directly. Dead file. |
none |
3. Frontend Architecture¶
Overall architecture¶
flowchart TB
subgraph Browser
subgraph "Survey Portal SPA"
MAIN["main.jsx - Provider + RouterProvider"]
ROUTER["HashRouter - routes/routes.jsx"]
LAYOUT["Layout/index.jsx - fetch questionnaire on mount"]
GUARD["StatusRoute.jsx - redirect if status===completed"]
PAGES["Pages: Language, Start, - QuizScreen, Review"]
COMPS["Components: Likert widgets, - FeedbackSubmission, SurveyCompletion"]
end
SVC["questionsService.js"]
AX["axiosInstance.js - bearer token from localStorage"]
STORE[("Redux store - survey slice")]
SESS[("sessionStorage - qstr_url, language")]
end
API[("Remote backend - dpak-ees-srv-26.engagesurvey.biz - /survey-api/*")]
MAIN --> ROUTER --> LAYOUT
LAYOUT --> GUARD --> PAGES
LAYOUT --> PAGES
PAGES --> COMPS
LAYOUT --> SVC
PAGES --> SVC
COMPS --> SVC
SVC --> AX --> API
LAYOUT --> STORE
PAGES --> STORE
COMPS --> STORE
LAYOUT --> SESS
PAGES --> SESS
Application lifecycle (from page load to a rendered survey question)¶
sequenceDiagram
participant B as Browser
participant M as main.jsx
participant R as HashRouter
participant L as Layout
participant SVC as questionsService
participant API as Backend /survey-api
participant ST as Redux (survey slice)
B->>M: Load index.html, execute main.jsx
M->>M: createRoot("#root").render("Provider + RouterProvider")
R->>L: Match "/" (or "/:id"), render Layout element
L->>L: Read :id param or sessionStorage("qstr_url")
alt no token found anywhere
L->>B: Render "Invalid URL" message, stop
else token found
L->>L: sessionStorage.setItem('qstr_url', id)
L->>SVC: fetchQuestions({ qstr_url })
SVC->>API: POST /survey-api/get-questionnaire
API-->>SVC: { res: { qstr, questions, scales } }
SVC-->>L: response.res
L->>ST: dispatch(setLanguage(res.qstr.lang))
L->>ST: dispatch(setData(res))
Note over ST: setData computes answers{}, currentQuestion,<br/>questionNo, status, endSuvery from server data
L->>B: Render <Outlet/> (StatusRoute → Language/Start/Quiz/Review)
end
Component architecture¶
- No container/presentational split by folder —
pages/holds route-level screens that both fetch/dispatch and render;components/holds a mix of pure-presentational primitives (Button,ProgressBar) and components that read Redux directly (CheckboxSelector,RoundedLikert,SmileyLikert,SquaredLikert,Slider,FeedbackSubmission,SurveyCompletionall calluseSelector((state) => state.survey)forlanguage). Layoutis the only data-fetching root. Every other page assumesstate.survey.questionsis already populated by the time it mounts (with one exception:Review.jsx'sReviewCardre-fetches ifquestionsis empty, e.g. after a hard refresh landing directly on/review).- One large page, several small answer widgets.
src/pages/quiz.jsx(489 lines) is by far the largest file in the app; it owns navigation, answer-submission, and a five-way conditional render of the answer widget for the current question's type.
Design patterns used¶
| Pattern | Where |
|---|---|
| Provider pattern | Redux Provider wraps RouterProvider in src/main.jsx:11-13 |
| Route-as-tree with a nested layout route | createHashRouter([{ path: "/", element: <Layout/>, children: [...] }]) — src/routes/routes.jsx:8-53 |
| Guard-wrapped route element | StatusRoute wraps Language and QuizScreen route elements directly in the route config rather than via a HOC or loader (src/routes/routes.jsx:14-19, 26-33) |
| Barrel exports | src/pages/index.jsx and src/components/index.jsx re-export their folder's default exports for shorter imports |
| Slice pattern (single slice) | createSlice in src/redux/slices/surveySlice.js holds the entire app's data model |
| Service module pattern | questionsService object with four async methods wraps apiClient, itself wrapping axiosInstance — three layers, each thin |
| Server-derived resume position | setData reducer recomputes currentQuestion/questionNo/endSuvery from how many answers the server returns, rather than persisting position purely client-side |
| sessionStorage as a secondary persistence layer | qstr_url and language are read as Redux initialState defaults directly from sessionStorage (src/redux/slices/surveySlice.js:9,13) |
4. Routing¶
Library & mode¶
React Router v6, using createHashRouter (src/routes/routes.jsx:1,8). URLs are hash-based (e.g. https://host/#/quiz). This is standard for a static-hosted SPA with no server-side rewrite configuration, and also plays well with invite links that carry the survey token as a route param (/#/:id).
Route tree (src/routes/routes.jsx)¶
flowchart TB
ROOT["/ → Layout (fetches questionnaire, renders Outlet)"]
ROOT --> IDX["/ (index) → StatusRoute → Language"]
ROOT --> TOK["/:id → Language (invite-link entry point)"]
ROOT --> QUIZ["/quiz → StatusRoute → QuizScreen"]
ROOT --> START["/start → Start"]
ROOT --> SUBMIT["/submit → FeedbackSubmission"]
ROOT --> REVIEW["/review → Review"]
ROOT --> COMPLETION["/completion → SurveyCompletion"]
All seven routes are children of the single layout route at /, so Layout's questionnaire-fetch effect runs on every navigation that causes a full route match under / (in practice, once per SPA session, since Layout itself doesn't remount on child navigation — only on a hard reload or when the :id param value changes, because it's keyed by the URL param dependency in its useEffect, src/Layout/index.jsx:24-66).
The route guard (src/routes/StatusRoute.jsx)¶
flowchart TB
R(["Route requested: '/' or '/quiz'"]) --> D{"state.survey exists?"}
D -->|No| ERR["console.error + Navigate to '/'"]
D -->|Yes| S{"status === 'completed'?"}
S -->|Yes| NAV["Navigate to '/completion'"]
S -->|No / null / anything else| RENDER["Render children"]
StatusRoute (src/routes/StatusRoute.jsx:5-21) is the app's only route guard. It:
1. Reads state.survey via useSelector.
2. If the slice is falsy (should not normally happen, since Redux always provides a default), logs an error and redirects to /.
3. If status === "completed" (a value that arrives from the backend inside qstr.status and is stored by setData, src/redux/slices/surveySlice.js:68), redirects to /completion — this prevents someone from reopening an already-submitted survey link and re-entering the quiz or language screen.
4. Otherwise renders its children unchanged.
It is applied only to / (Language, index route) and /quiz. It is not applied to /start, /review, /submit, or /completion — those are reachable at any time regardless of status, relying instead on each page's own guards against missing data (e.g. Review.jsx:108 renders "loading..." if answers is falsy).
Public vs. protected routes¶
There is no authentication in this app (see §11), so "protected" here means "gated by survey completion status," not by login:
| Route | Guarded by StatusRoute? |
Notes |
|---|---|---|
/ (index) |
Yes | Entry point when no per-employee token is in the URL; also the default landing after redirect |
/:id |
No | The actual invite-link shape (https://host/#/<token>); Language renders directly, no completion check — see §22 for the implication |
/quiz |
Yes | Cannot be reached if the survey is already completed |
/start |
No | Welcome/instructions screen |
/review |
No | Answer review grid |
/submit |
No | Final confirm screen (FeedbackSubmission) |
/completion |
No | Thank-you screen; the redirect target, not itself guarded |
Fallback / catch-all route¶
None. The route tree has no path: "*" entry, so any unmatched hash path renders nothing under <Outlet/> inside Layout (the header/footer chrome still renders, but the content area is blank). This is a gap relative to typical SPA routing hygiene — see §22.
Lazy-loaded routes¶
None found. All page and component modules are statically imported at the top of src/routes/routes.jsx and src/pages/index.jsx. No React.lazy/Suspense code-splitting is used.
5. The Survey Flow¶
This is the core "module" of the application — the end-to-end employee journey. It is a strictly linear wizard with one branch point (resume-in-progress vs. already-completed) and one loop (question N → question N+1 until the last question, or back-and-forth via Review's edit links).
End-to-end flow¶
stateDiagram-v2
[*] --> Layout: Employee opens invite link
Layout --> FetchQuestionnaire: POST /survey-api/get-questionnaire
FetchQuestionnaire --> StatusCheck
state StatusCheck <<choice>>
StatusCheck --> Completion: status is completed
StatusCheck --> Language: status is not completed
Language --> Language: POST /survey-api/select-language per click
Language --> Start: Continue, status not completed
Language --> Completion: Continue, status completed - stale state
Start --> Quiz: Start button, navigate to quiz
state Quiz {
[*] --> AnswerQuestion
AnswerQuestion --> SubmitAnswer: Next or answer selected
SubmitAnswer --> AnswerQuestion: POST submit-answer, questionNo plus 1
AnswerQuestion --> [*]: last question reached
}
Quiz --> Review: Submit on last question - after final submit-answer call
Review --> Quiz: pencil icon on a card - navigate with reviewQuesNo state
Quiz --> Review: Back to Review, only in review-edit mode
Review --> FeedbackSubmission: Finish
FeedbackSubmission --> Review: Review button
FeedbackSubmission --> Completion: Complete button - POST submit-questionnaire, sessionStorage clear
Completion --> [*]
Step-by-step, with the exact code paths¶
-
Entry (
Layout,src/Layout/index.jsx:24-66). On mount, resolves the survey token from the:idroute param orsessionStorage.getItem("qstr_url"). If neither exists, shows "Invalid URL" and stops. Otherwise saves the token tosessionStorageand callsquestionsService.fetchQuestions({ qstr_url }). On success, dispatchessetLanguage(res.qstr.lang)thensetData(res)— the latter populatesquestions,answers,currentQuestion,questionNo,status, andendSuveryin one shot (src/redux/slices/surveySlice.js:20-78). A full-screen spinner (.loader,src/Layout/Layout.css) is shown while this is in flight. -
Language selection (
Language,src/pages/Language.jsx). Two buttons ("English" / "اردو"). Clicking one callsquestionsService.selectLanguage({ qstr_url, lang })(src/pages/Language.jsx:22-25) and, on resolution, dispatchessetLanguage(lang)— note the dispatch happens regardless of whether the API call actually succeeded server-side vs. just not throwing (there is atry/catchbut no distinct success/failure branch for the dispatch). The "Continue" button is disabled until a language is chosen; clicking it navigates to/start, or to/completionifstatus === "completed"(belt-and-suspenders alongsideStatusRoute). -
Welcome/instructions (
Start,src/pages/Start.jsx). Fetchespublic/branding.jsonclient-side via plainfetch(not throughquestionsService/axiosInstance— this is a same-origin static asset, not an API call) and renders itswelcome.contentarray of{en, ur}paragraph pairs, switched bystate.survey.language. The "Start" button navigates to/quiz. -
Quiz loop (
QuizScreen,src/pages/quiz.jsx). For the question atstate.survey.questionNo: - Renders one of five answer widgets based on
currentQuestion.questionType(round_slider→RoundedLikert,nps_slider→Slider,box→SmileyLikert,square_slider→SquaredLikert,integrity→CheckboxCard). - Selecting an answer calls
handleAnswer, which builds an answer object ({ans_id, qst_id, type, score, questionNo, other_text?, ans_state}) and dispatchessetAnswer({questionId: questionNo, answer})— this only updates local Redux state, it does not call the API. - Clicking Next (
handleNext) is what actually POSTs to the backend:questionsService.submitAnswer({qstr_url, ans}), then on success dispatchessetCurrentQuestionNo(questionNo + 1). If no answer exists yet, a SweetAlert2 warning blocks progression. - Clicking Previous (
handlePrevious) is purely local —setCurrentQuestionNo(questionNo - 1)— no API call. - On the last question, the Submit button calls
handleSubmit, which callshandleNext(null)(submitting the current answer) and then navigates to/review. - Review-edit mode: if the user arrived via a pencil icon from
Review,location.state.reviewQuesNois present; auseEffectdispatchessetCurrentQuestionNo(reviewQuesNo)(src/pages/quiz.jsx:45-49), and the button row swaps to show Back to Review (handleReview, which submits the current answer then navigates to/review) instead of the normal Next/Submit flow. -
A "Save & Resume Later" button is always visible and simply navigates to
/(src/pages/quiz.jsx:401-407) — since the token is already insessionStorageand the backend already has each answer submitted as it's given, reopening the same link later resumes at the correct question viaLayout's fetch +setData's resume-position calculation. -
Review (
Review,src/pages/Review.jsx). Renders oneReviewCardper question in a responsive grid, each showing the question text and the selected answer's label (matched by filteringquestion.optionsforvalue === answers[i].score). Each card has an edit (pencil) icon that navigates to/quizwith{state: {reviewQuesNo: index}}. Ifstate.survey.questionsis empty (e.g., a hard refresh landed directly on/review),ReviewCard's ownuseEffectre-fetches viaquestionsService.fetchQuestionsand dispatchessetDataagain (src/pages/Review.jsx:23-37) — this is a second, independent fetch path outsideLayout. The "Finish" button navigates to/submit. -
Final confirmation (
FeedbackSubmission,src/components/FeedbackSubmission.jsx, routed at/submit). Client-side sanity check that every index inanswersis truthy (loops0..questions.length-1); if any is missing, shows a SweetAlert2 warning and blocks. The "Review" button goes back to/review. The "Complete" button (handleComplete) callsquestionsService.submitSurvey({qstr_url}), thensessionStorage.clear()(wiping bothqstr_urlandlanguage), then navigates to/completion. On API failure, shows a SweetAlert2 error with a "Retry" label (the button itself does not actually retry automatically — the user must click "Complete" again). -
Completion (
SurveyCompletion,src/components/SurveyCompletion.jsx, routed at/completion). Static thank-you screen using thetranslationsobject (en.json/ur.json) for its copy. No further navigation or API calls. This is also the screenStatusRouteredirects to if the employee reopens a completed survey's link.
Data flow per answer¶
sequenceDiagram
participant U as Employee
participant Q as QuizScreen
participant ST as Redux (survey slice)
participant SVC as questionsService
participant API as Backend
U->>Q: Selects an answer option
Q->>ST: dispatch(setAnswer({questionId, answer})) — local only
U->>Q: Clicks "Next"
Q->>SVC: submitAnswer({ qstr_url, ans })
SVC->>API: POST /survey-api/submit-answer
API-->>SVC: response
SVC-->>Q: response
Q->>ST: dispatch(setCurrentQuestionNo(questionNo + 1))
ST-->>Q: currentQuestion updates, re-render
6. Pages¶
Pages live in src/pages/ and are re-exported from src/pages/index.jsx:6. Two additional route-level screens (FeedbackSubmission, SurveyCompletion) live under src/components/ instead but function identically as full-screen routed views — they are documented in §7 but cross-referenced here since they are part of the page flow.
src/pages/Language.jsx — Language selection¶
- Route:
/(index, guarded) and/:id(unguarded, invite-link entry). - Reads:
state.survey.language,state.survey.status. - Local state:
loading(boolean, disables both language buttons and swaps the Continue label to "Continue….." while aselectLanguagecall is in flight). - API calls:
questionsService.selectLanguage({ qstr_url, lang })on each language button click. - Dispatches:
setLanguage(lang). - Navigation:
/startnormally, or/completionifstatus === "completed". - Notable: renders both English and Urdu prompt text simultaneously (not switched) so the employee can read either script before choosing — the Urdu button label itself uses the bundled
Jameel Noori Nastaleeqfont inline (src/pages/Language.jsx:71).
src/pages/Start.jsx — Welcome / instructions¶
- Route:
/start(unguarded). - Reads:
state.survey.language. - Local state:
welcome(the parsedbranding.json,undefineduntil loaded — component rendersnulluntil then, i.e. a blank screen with no spinner). - Side effect: plain
fetch("/branding.json")on mount (not viaquestionsService). - Navigation:
/quizvia the "Start"/"شروع کریں" button. - Notable: the bank name and survey year ("DUBAI ISLAMIC BANK EMPLOYEE ENGAGEMENT SURVEY 2026") are hardcoded English/Urdu strings in this file (
src/pages/Start.jsx:48-49), not sourced frombranding.jsonor the locale files — a second, page-local translation mechanism alongsideen.json/ur.jsonandbranding.json.
src/pages/quiz.jsx — QuizScreen, the question-answering loop¶
- Route:
/quiz(guarded byStatusRoute). - Reads: the full
state.surveyobject (questions,loading,currentQuestion,questionNo,qstr_url,answers,language). - Local state:
isProcessing(guards against double-submitting an answer while asubmitAnswercall is in flight). - API calls:
questionsService.submitAnsweron Next/Submit/Back-to-Review. - Dispatches:
setAnswer,setCurrentQuestionNo. - Navigation:
/submit(after last question),/review(Back to Review, edit mode only),/(Save & Exit). - Notable: the single largest file in the app (489 lines); see §5 step 4 for full behavior and §7 for the five answer widgets it switches between.
src/pages/Review.jsx — Answer review grid¶
- Exports:
Review(default) and an inlineReviewCardsub-component (not separately exported/reused elsewhere). - Route:
/review(unguarded). - Reads:
state.survey.answers,state.survey.questions,state.survey.language(both inReviewand independently in eachReviewCard). - API calls:
ReviewCardcallsquestionsService.fetchQuestionsonly ifquestionsis empty when it mounts (defensive re-hydration for direct/refreshed navigation to/review). - Dispatches:
setData(from the defensive re-fetch above). - Navigation:
/quizwith{state: {reviewQuesNo: index}}per card's edit icon;/submitvia "Finish". - Notable: matches the displayed answer label by filtering
question.optionsforoption.value === answers[i].score(src/pages/Review.jsx:168-171) — if an answer's score doesn't match any option'svalue(e.g., free-text-only "integrity" answers), the card would show a blank/undefined label; see §22.
src/pages/index.jsx — Pages barrel¶
Simple re-export file: export { QuizScreen, Language, Start, Review }; — note QuizScreen is the named export for the default export of quiz.jsx.
7. Components¶
Location: src/components/ (+ src/components/LikertScale/ subfolder). Barrel export: src/components/index.jsx (note it does not export Demographics, QuizTimer, FeedbackSubmission, or SurveyCompletion — those four are imported by their direct file paths where used, or in the case of Demographics/QuizTimer, not used at all).
7.1 Generic / reusable¶
| Component | File | Props | Responsibility |
|---|---|---|---|
| Button | Button.jsx |
onClick, icon (component), children, disabled, className |
The app's single button primitive — a styled <button> with an optional trailing icon. Used by every page/screen. |
| Modal | Modal.jsx (+ Modal.css) |
isOpen, onClose, title, children |
A centered overlay dialog with a close (×) icon. Not referenced by any page or other component in this codebase — authored but currently unused. |
| Accordion | Accordion.jsx |
title, children |
Expand/collapse panel with chevron icon, local isOpen state. Not referenced anywhere else in the codebase — unused. |
| DropDown | DropDown.jsx (+ DropDown.css) |
title, options, onSelect, selectedOption, language |
A custom (non-native) <select>-style dropdown, language-aware label rendering (name_eng/name_urdu). Used only by Demographics (itself unused — see below). |
| ProgressBar | ProgressBar.jsx (+ .css) |
currentStep, totalSteps |
Renders a thin percentage-fill bar plus an "N/Total" label. Used in quiz.jsx to show question progress. Has a mobile-specific position override in ProgressBar.css. |
| RadioSelector | RadioSelector.jsx (+ .css) |
title, options, onSelect, selectedOption |
A horizontally/vertically responsive radio-button group. Used only by Demographics (unused — see below). |
| CheckboxSelector | CheckboxSelector.jsx |
quesData, selectedOption, onSelect, options |
Single-select "checkbox-styled" option list with optional nested free-text follow-up textarea (shown when the selected option has an oef_followup_eng/oef_followup_urdu field). Reads state.survey.language directly. |
| CheckboxCard | CheckboxCard.jsx |
selectedOption, onSelect, quesData |
Thin wrapper around CheckboxSelector; this is the component actually rendered by quiz.jsx for questionType === "integrity". |
7.2 Survey-specific¶
| Component | File | Props | Responsibility |
|---|---|---|---|
| RoundedLikert | LikertScale/RoundedLikert.jsx |
quesData, selectedOption, onSelect |
A horizontal line of round check-mark selectors evenly spaced (index * 24.18%) with labels below. Rendered by quiz.jsx for questionType === "round_slider". Reads state.survey.language for label switching and RTL font. |
| SquaredLikert | LikertScale/SquaredLikert.jsx |
selectedOption, quesData, onSelect |
Visually identical layout logic to RoundedLikert but with square (rounded-rect) selector markers. Rendered for questionType === "square_slider". |
| SmileyLikert | LikertScale/SmileyLikert.jsx |
selectedOption, quesData, onSelect |
A responsive grid of image-based "smiley" cards (public/smileyLikert/{value}.png), highlighted on hover/selection. Rendered for questionType === "box". Wrapped in React.memo. |
| Slider | Slider.jsx |
quesData, selectedOption, onSelect |
A draggable (mouse + touch) horizontal 0–10 NPS-style slider with option labels and an optional nested follow-up question/textarea driven by quesData.followups[].values matching the selected score. Rendered for questionType === "nps_slider". Contains ~225 lines of commented-out prior implementation left in place above the active code (Slider.jsx:1-227). |
| Demographics | Demographics.jsx |
quesData, selectedOption, onSelect |
Switches between RadioSelector (quesType === "radio") and DropDown (quesType === "list") for demographic-style questions. Not imported by any page or route — confirmed via full-codebase search; this component, and the two selectors it depends on for its intended purpose, are present in source but dead code in the current build (quiz.jsx's question-type switch has no branch that renders Demographics). |
| QuizTimer | QuizTimer.jsx |
none | A self-contained 15-minute countdown display ("Time: NN mins"), decrementing every second via setInterval. Not imported anywhere — dead code. Has no visible behavior on expiry (no auto-submit, no warning) even if it were wired in. |
| FeedbackSubmission | FeedbackSubmission.jsx |
none (route-level) | The pre-submit confirmation screen, routed at /submit. See §5 step 6 and §6 note above. |
| SurveyCompletion | SurveyCompletion.jsx |
none (route-level) | The terminal thank-you screen, routed at /completion. See §5 step 7. |
Dead / unused component summary¶
Confirmed by grepping all of src/ for each component's usage: Demographics, QuizTimer, Modal, and Accordion are authored components with no import sites outside their own file (and, for Demographics, its own dependency chain to RadioSelector/DropDown, which are otherwise also unused). This suggests either an in-progress demographics-collection feature that was descoped/paused, or a UI-timer feature that was built but never wired into quiz.jsx. Any developer touching these should confirm with the product owner whether they are planned for re-activation before deleting or extending them.
8. State Management¶
Approach¶
Redux Toolkit, with a single hand-written slice (survey) and no RTK Query, no middleware beyond Redux Toolkit's defaults, and no persistence library (redux-persist is not a dependency). Cross-reload continuity is instead achieved by re-fetching from the backend on every Layout mount, using a token cached in sessionStorage.
Store shape (src/redux/store.js + src/redux/rootReducer.js)¶
flowchart TB
subgraph "Redux Store (configureStore)"
subgraph "rootReducer (combineReducers)"
SURVEY["survey: surveySlice.reducer"]
end
end
classDef slice fill:#f3e5f5,stroke:#6a1b9a;
class SURVEY slice;
src/redux/store.js:4-6 calls configureStore({ reducer: rootReducer }) with no additional middleware, enhancers, or preloaded state. src/redux/rootReducer.js:5-7 combines exactly one reducer key, survey, mapped to surveySlice.js's default export.
The survey slice — full state shape (src/redux/slices/surveySlice.js:4-14)¶
{
loading: false, // set true while Layout's fetchQuestions is in flight
questions: [], // array of question objects, enriched with `options` + `questionType` by setData
currentQuestion: null, // the question object at index `questionNo`
questionNo: 0, // current position in the quiz
qstr_url: sessionStorage.getItem("qstr_url") || null, // the survey/invite token
answers: {}, // map: questionIndex (int, as object key) -> answer object
endSuvery: false, // true once every question has a saved answer (note: typo preserved from source)
status: null, // "completed" | other values from qstr.status
language: sessionStorage.getItem("language") || "eng" // "eng" | "urdu"
}
Each question object, after setData enrichment¶
{
// ...raw fields from the backend (ans_id, qst_id, scale_id, qst_eng, qst_urdu, type, score, other_text, ans_state, has_followup, followups, ...)
options: [...], // resolved from `scales` by matching scale_id, defaulting to []
questionType: "round_slider" | "nps_slider" | "box" | "square_slider" | "integrity" | "unknown"
}
Each answer object (as stored in state.survey.answers[questionNo])¶
{
ans_id,
qst_id,
type,
score, // the selected option's numeric value
questionNo, // (added by handleAnswer in quiz.jsx; not present on answers pre-populated by setData)
other_text, // present only for "nps" (per setData) or "integrity"/"nps_slider" question types (per handleAnswer) — otherwise stripped
ans_state // "yes" | null
}
Actions & reducers¶
| Action | Payload | Reducer behavior |
|---|---|---|
setData |
{ questions, scales, qstr } (the raw API response's res object) |
Validates questions is an array and scales is an object (else logs an error and no-ops). Maps each question to attach options/questionType from the matching scale. For each question that already has a server-side answer (ans_state != null), reconstructs an answer object and stores it in answers[index], counting it toward quesNo. Sets status from qstr.status. Computes currentQuestion/questionNo as the first unanswered question (or the last question, if all are answered). Sets endSuvery = true if every question is answered. Re-reads qstr_url from sessionStorage (redundant with the initial state default, but re-applied here too). Sets loading = false. |
setLoading |
boolean | Bug: assigns to a local tempState copy and reassigns the state parameter reference (state = tempState) instead of mutating via Immer or returning tempState — this has no effect on the actual store; loading is never updated by this action as written (src/redux/slices/surveySlice.js:80-84). See §22. |
setAnswer |
{ questionId, answer } |
state.answers[questionId] = answer — local-only, no API call (the caller in quiz.jsx issues the API call separately via questionsService.submitAnswer). |
reviewAnswers |
none | Returns { ...state } — a no-op spread with no observable effect. Not dispatched anywhere in the codebase (dead action). |
setLanguage |
"eng" \| "urdu" |
Returns { ...state, language: action.payload }. Also logs the payload to console (console.log(action.payload, "actions"), src/redux/slices/surveySlice.js:95) — a leftover debug statement. Does not persist to sessionStorage despite language being read from sessionStorage at initial-state time — see §22. |
updateAnswer |
{ questionId, newAnswer } |
state.answers[questionId] = newAnswer — functionally identical to setAnswer with different payload key names. Not dispatched anywhere in the codebase (dead action; setAnswer is used instead throughout). |
setCurrentQuestionNo |
integer | If in range [0, questions.length), sets questionNo and currentQuestion to match; otherwise logs a warning and no-ops. Drives all forward/backward/review-jump navigation within the quiz. |
Data flow (typical answer-submission cycle)¶
sequenceDiagram
participant C as QuizScreen (component)
participant D as dispatch
participant SL as surveySlice reducer
participant ST as Redux store
participant SVC as questionsService
participant API as Backend
C->>D: dispatch(setAnswer({questionId, answer}))
D->>SL: apply setAnswer reducer
SL->>ST: state.answers["questionId"] = answer
ST-->>C: re-render (Likert widget shows selection)
C->>SVC: submitAnswer({qstr_url, ans}) (on Next click)
SVC->>API: POST /survey-api/submit-answer
API-->>SVC: response
C->>D: dispatch(setCurrentQuestionNo(questionNo + 1))
D->>SL: apply setCurrentQuestionNo reducer
SL->>ST: questionNo, currentQuestion updated
ST-->>C: re-render (next question shown)
Global vs. local state¶
| State kind | Where | Example |
|---|---|---|
| Global | The survey slice |
Questions, answers, current position, language, completion status |
| Local | useState in a page/component |
Language's loading, Start's welcome, quiz.jsx's isProcessing, CheckboxSelector's answer (free-text draft), Slider's drag state |
9. API Layer¶
Architecture¶
flowchart TB
C["Component / Page"] -->|calls| SVC["questionsService - src/services/questionsService.js"]
SVC -->|uses| AC["apiClient - src/api/apiClient.js"]
AC -->|delegates to| AX["axiosInstance - src/api/axiosInstance.js"]
AX -->|request interceptor| TOK["Reads localStorage 'authToken' - adds Authorization: Bearer"]
AX -->|response interceptor, on error| ERR["handleError - src/api/errorHandling.js"]
AX -->|HTTPS| API[("dpak-ees-srv-26.engagesurvey.biz - /survey-api/*")]
axiosInstance.js (src/api/axiosInstance.js)¶
const axiosInstance = axios.create({
baseURL: "https://dpak-ees-srv-26.engagesurvey.biz", // hardcoded, production host, no env var
timeout: 20000,
headers: { "Content-Type": "application/json" },
});
- A commented-out
baseURL: "/api"(axiosInstance.js:5) suggests a dev-proxy setup was considered or previously used but is currently disabled in favor of hitting production directly from any environment. - Request interceptor (
axiosInstance.js:11-20): readslocalStorage.getItem("authToken")and, if present, setsAuthorization: Bearer <token>on every outgoing request. There is no code anywhere in this codebase that writesauthTokentolocalStorage— see §11. - Response interceptor (
axiosInstance.js:22-25): passes successful responses through unchanged; on error, delegates tohandleError. - No token-refresh logic, no request retry, no request/response logging beyond what individual call sites add manually.
apiClient.js (src/api/apiClient.js)¶
A minimal, documented wrapper exposing get, post, put, delete, each a one-line passthrough to the corresponding axiosInstance method. Only post is actually used anywhere in the app (via questionsService); get, put, and delete are unused but kept as a complete CRUD surface.
errorHandling.js (src/api/errorHandling.js)¶
export const handleError = (error) => {
const errorMessage =
error?.response?.data?.message ||
error?.message ||
"An unexpected error occurred";
return Promise.reject({
message: errorMessage,
details: error?.response?.data || error,
});
};
Normalizes any Axios error into { message, details }. It is wired as the Axios response interceptor's error handler (so it fires for every failed request automatically), and is also imported and called a second time, redundantly, inside a couple of questionsService methods' own catch blocks (submitSurvey, selectLanguage) — see §18.
questionsService.js (src/services/questionsService.js) — the complete API surface¶
| Method | Endpoint | Payload (as constructed by the caller) | Used by | Response usage |
|---|---|---|---|---|
fetchQuestions(payload) |
POST /survey-api/get-questionnaire |
{ qstr_url } |
Layout (on mount), Review.jsx's ReviewCard (defensive re-fetch) |
response.data returned to caller; callers read response.res (i.e., the backend wraps its payload in a top-level res key) and pass it to setData |
submitAnswer(payload) |
POST /survey-api/submit-answer |
{ qstr_url, ans } where ans is the current question's answer object |
quiz.jsx (handleNext, handleReview) |
Response is logged but not otherwise consumed; success just allows navigation/state update to proceed |
submitSurvey(payload) |
POST /survey-api/submit-questionnaire |
{ qstr_url } |
FeedbackSubmission (handleComplete) |
Response is logged; on success, sessionStorage.clear() runs and the app navigates to /completion |
selectLanguage(payload) |
POST /survey-api/select-language |
{ qstr_url, lang } |
Language.jsx (handleLanguageSelect) |
Response is logged; setLanguage(lang) dispatches regardless |
Backend contract note: all four endpoints are inferred purely from these call sites — request shape as constructed here, and response usage as consumed here (response.res for fetchQuestions, otherwise just presence/absence of a thrown error). The backend's actual implementation, validation rules, auth requirements, and full response schemas are out of scope — see §25.
Request lifecycle¶
sequenceDiagram
participant P as Page/Component
participant SVC as questionsService
participant AC as apiClient
participant AX as axiosInstance
participant API as Backend
P->>SVC: questionsService.submitAnswer(payload)
SVC->>AC: apiClient.post('/survey-api/submit-answer', payload)
AC->>AX: axiosInstance.post(url, payload)
AX->>AX: request interceptor adds Bearer token (if any)
AX->>API: HTTPS POST
alt success
API-->>AX: 2xx + JSON
AX-->>AC: response
AC-->>SVC: response
SVC-->>P: response.data
else failure
API-->>AX: error
AX->>AX: response interceptor -> handleError("error")
AX-->>SVC: rejected Promise { message, details }
SVC-->>P: re-thrown or returned via handleError (inconsistent — see §18)
end
10. Forms & Validation¶
There is no form library (no Formik/React Hook Form/Yup). All input is either single-choice selection widgets or free-text <textarea> follow-ups, validated with plain conditional checks:
| Validation | Where | Behavior |
|---|---|---|
| Answer required before advancing | quiz.jsx handleNext (src/pages/quiz.jsx:64-80) |
If no answer exists for the current question (or qstr_url is missing), shows a SweetAlert2 warning ("Incomplete Answer") and blocks navigation. |
| Next/Submit button disabled state | quiz.jsx isDisabled (src/pages/quiz.jsx:186-189) |
Computed from whether answers[questionNo].score is falsy-and-not-zero, or other_text is null/"". Disables the Next/Submit/Back-to-Review buttons visually and via the disabled prop. |
| All-questions-answered check before final submit | FeedbackSubmission.jsx handleComplete (src/components/FeedbackSubmission.jsx:33-49) |
Loops every question index and checks answers[i] truthiness; shows a SweetAlert2 warning if any is missing. |
| Free-text follow-up presence | CheckboxSelector.jsx, Slider.jsx |
Follow-up <textarea> only rendered conditionally when the selected option has a matching oef_followup_* field (CheckboxSelector) or followups[].values includes the selected score (Slider) — not separately required/validated beyond the general isDisabled check above. |
No client-side validation library, no field-level error messages, no schema validation. All "form" feedback is either a button disabled state or a SweetAlert2 modal. There is no dedicated inline error-message component analogous to a FormError — errors surface only via Swal.fire(...) calls scattered in quiz.jsx and FeedbackSubmission.jsx.
User feedback mechanisms¶
- Blocking warnings/errors:
sweetalert2(Swal.fire) — used for incomplete-answer, submission-failure, and incomplete-survey cases. Each call site defines its own inlinedidRender: ButtonStylescallback to recolor the confirm button (duplicated inquiz.jsxandFeedbackSubmission.jsxwith different colors —#004899vs#CE153F). - Loading feedback: the "Continue….." label swap in
Language.jsx, the full-page spinner inLayout, and theisProcessingguard inquiz.jsx(no visible spinner, just click-prevention). - No toast/snackbar system — everything is either an inline UI state or a full modal dialog.
11. Authentication & Session¶
There is no login flow¶
This app has no sign-in screen, no credentials form, and no user-identity concept beyond "whoever holds this survey link." Access control is entirely link-based:
- An employee receives a unique URL containing a token (the
qstr_url), presumably generated and distributed by the backend/consultancy out of band (email invite, etc. — not part of this codebase). - The SPA reads that token from the route (
/:id→useParams().id,src/Layout/index.jsx:11,32) on first load, or fromsessionStorage.getItem("qstr_url")on any subsequent load within the same browser tab/session (src/Layout/index.jsx:31). - Every subsequent API call includes this token in its payload (
{ qstr_url, ... }) — the token is the access credential from the frontend's point of view; the backend presumably uses it to look up which respondent/questionnaire instance is being addressed. (Backend-side validation/expiry behavior is out of scope — see §25.)
The authToken / Bearer-token mechanism is present but unused¶
src/api/axiosInstance.js:13-16 reads localStorage.getItem("authToken") and attaches it as an Authorization: Bearer header on every request if present. A full-codebase search finds no call site anywhere in src/ that writes to localStorage.setItem("authToken", ...). This means:
- In the current build, every outgoing request is sent without an Authorization header (since authToken is never populated by this frontend).
- The interceptor code is either a leftover from a shared template (plausible given the axios/apiClient/errorHandling three-file pattern mirrors typical boilerplate), or scaffolding for a not-yet-implemented auth mechanism (e.g., the backend may issue a token during select-language or get-questionnaire that a future version of this app is meant to persist).
- This is flagged explicitly rather than guessed at further — see §25.
Session persistence¶
| Key | Storage | Set by | Read by | Cleared by |
|---|---|---|---|---|
qstr_url |
sessionStorage |
Layout.saveQuestionToSession (src/Layout/index.jsx:18-22) |
surveySlice initial state, Layout, quiz.jsx, Review.jsx's ReviewCard, FeedbackSubmission.jsx |
FeedbackSubmission.handleComplete via sessionStorage.clear() (src/components/FeedbackSubmission.jsx:56) |
language |
sessionStorage |
Never explicitly written — only ever read as an initialState default (src/redux/slices/surveySlice.js:13); the setLanguage reducer updates Redux state but does not call sessionStorage.setItem |
surveySlice initial state only |
sessionStorage.clear() (same call as above) |
authToken |
localStorage |
Never written anywhere in this codebase | axiosInstance request interceptor |
n/a |
Because sessionStorage (not localStorage) holds qstr_url, the survey token does not persist across browser tabs or after the tab is closed — closing and reopening the same invite link in a fresh tab relies entirely on the :id route param being present in that URL again, not on any cached session state.
"Protected" pages¶
As covered in §4, the only access control is StatusRoute's completion-status check — there is no permission/role system, since there is only one user type (the survey respondent) and no admin surface in this codebase.
12. Internationalization¶
Two coexisting i18n mechanisms¶
The app ships two parallel translation systems that do not share a single source of truth:
- i18next / react-i18next (
src/i18n.js) — properly initialized withen/urresource bundles loaded fromsrc/locales/en.jsonandsrc/locales/ur.json, default language"en", fallback"en", andescapeValue: false(safe, since React already escapes). However,i18n.jsis never imported bymain.jsxor any other entry point in the codebase — a search confirms no file imports"./i18n"or"../i18n". This means i18next is never actually initialized at runtime, and no component uses theuseTranslation()hook anywhere insrc/. - Manual JSON-object language switching — the pattern actually used everywhere. Components import
translationsEN/translationsURdirectly fromsrc/locales/en.json/ur.jsonas plain JS objects (e.g.src/pages/quiz.jsx:6-7,src/pages/Review.jsx:7-8,src/components/FeedbackSubmission.jsx:6-7,src/components/SurveyCompletion.jsx:3-4), pick one object based onlanguage === "urdu" ? translationsUR : translationsEN, and reference keys liketranslations.nextdirectly in JSX.
src/locales/en.json / ur.json — shape¶
Both files are flat, single-level key→string maps with identical key sets (16 keys): next, submit, previous, backToReview, instruction, complete, review, questionsReview, reviewInstructions, finish, thankYouTitle, thankYouMessage, closeWindowMessage, saveAndExit. These cover only UI chrome (buttons, review/submission copy) — question text and answer-option labels are not in these files; they come from the backend as qst_eng/qst_urdu and name_eng/name_urdu fields on each question/option object (see §8).
A third source of translated copy: public/branding.json¶
Start.jsx fetches public/branding.json at runtime for the welcome-screen paragraphs, each entry shaped { en: "...", ur: "..." } (see §2 and §6). This is a third, independent bilingual-content mechanism alongside locales/*.json and the hardcoded strings in Start.jsx itself.
Language-switching flow¶
sequenceDiagram
participant U as Employee
participant L as Language.jsx
participant SVC as questionsService
participant API as Backend
participant ST as Redux (survey.language)
U->>L: Clicks "English" or "اردو" button
L->>SVC: selectLanguage({ qstr_url, lang: "eng"|"urdu" })
SVC->>API: POST /survey-api/select-language
API-->>SVC: response (logged, not otherwise used)
L->>ST: dispatch(setLanguage(lang))
Note over ST: sessionStorage is NOT updated here - only the initial-state read at slice creation ever touches sessionStorage language
Note that Layout's initial fetch also dispatches setLanguage(response.res.qstr.lang) (src/Layout/index.jsx:49) — so the backend's stored language preference (presumably set by a prior select-language call, e.g. from a previous session) takes precedence on every fresh load, potentially overwriting whatever the Language page's local dispatch just set if the two race (in practice they don't race, since Layout's fetch happens once on mount before Language renders any buttons).
RTL considerations¶
There is no CSS-level dir="rtl" on any container element or the document root — the app does not use Tailwind's RTL plugin or a global direction: rtl. Instead, RTL is applied per-element, inline, wherever Urdu text is rendered: style={{ direction: "rtl", textAlign: "right", fontFamily: "Jameel Noori Nastaleeq", ... }} is repeated as an inline style object across nearly every page/component that renders question or answer text conditioned on language !== "eng" (e.g. src/pages/quiz.jsx:213-222, src/pages/Review.jsx:52-60, src/components/CheckboxSelector.jsx:97-107). This is a manual, per-component pattern rather than a systemic RTL layout switch — layout containers (flex/grid direction, padding/margin sides) are largely not mirrored for RTL, only text alignment and direction within text blocks.
The custom Urdu font¶
src/assets/fonts/JameelNooriNastaleeqRegular.ttf is declared via @font-face in src/index.css:3-6 under the family name "Jameel Noori Nastaleeq", and referenced by inline fontFamily styles throughout Urdu-rendering code paths (not applied via a Tailwind utility class or a CSS class — always inline style={{ fontFamily: "..." }}).
13. Shared Utilities¶
There is no src/hooks/ or src/utils/ directory in this codebase. All logic is inlined directly in the pages/components that need it — there are no custom hooks (no useDebounce, no typed Redux hooks, no useFormValidation) and no extracted pure-function helper modules. Examples of logic that would typically be extracted but is not:
- The
ButtonStylesSweetAlert2 confirm-button recoloring callback is duplicated (with different colors) in bothquiz.jsxandFeedbackSubmission.jsxrather than shared. - Option-sorting (
[...options].sort((a, b) => a.value - b.value)) is repeated independently inCheckboxSelector.jsx,RoundedLikert.jsx,SmileyLikert.jsx,SquaredLikert.jsx, andSlider.jsx. - The Urdu inline-style objects described in §12 are repeated, with minor variations, in nearly every component rather than centralized as a shared style constant or hook.
This is a genuine gap relative to typical React project structure and is called out again in §22 and §23.
14. Styling System¶
Approach — Tailwind utility-first, with a handful of plain .css files for what Tailwind can't easily express¶
flowchart LR
A["Tailwind CSS v3 - utility classes"] --> D["Final UI"]
B["Plain .css files - per component"] --> D
C["Inline style objects - RTL/Urdu overrides"] --> D
E["tailwind.config.js - custom primary palette"] --> A
- TailwindCSS v3 (
tailwind.config.js) is the primary styling mechanism — nearly every element uses Tailwind utility classes directly in JSXclassNamestrings.contentis scoped to./index.htmland./src/**/*.{js,ts,jsx,tsx}(tailwind.config.js:3). - A custom
primarycolor scale is defined as the app's brand palette:primary.DEFAULT = "#00723F"(green), pluslight,pale,mid,dark,extraLight,warmLightshades, asecondarygray (#8B8989),accent(#34D399), andneutral(#374151) (tailwind.config.js:6-19). A custommontserratfont-family token is also declared, though most components use the globalbodyfont-family fromindex.cssrather than thefont-montserratutility class explicitly. - Plain
.cssfiles sit alongside their component, imported directly (import "./DropDown.css", etc.) — this is a lightweight, hand-rolled analog to CSS Modules, but without actual module scoping:DropDown.cssandModal.cssboth define an identical.hide-scrollbarclass independently (duplicated, not shared), meaning both rely on global class-name uniqueness rather than true encapsulation. Files:Layout/Layout.css(spinner keyframes),components/DropDown.css,components/Modal.css,components/ProgressBar.css(a mobile breakpoint override),components/RadioSelector.css(a mobile breakpoint override). - Inline
style={{...}}objects are used extensively and are not incidental — they carry real conditional logic (RTL direction/font-family/spacing switches based onlanguage, dynamicleft: ${index * 24.18}%positioning for Likert selectors, dynamic width percentages forProgressBar). This is a third styling layer beyond Tailwind classes and the.cssfiles.
Global styles (src/index.css)¶
- Imports the Google Font
Montserrat(weights 400/600/700) via@import url(...). - Declares the
@font-facefor the bundledJameel Noori NastaleeqUrdu font. - The three
@tailwinddirectives (base,components,utilities). - Sets
body { font-family: "Montserrat", sans-serif; }as the default.
Responsive design¶
Tailwind responsive prefixes (sm:, md:, lg:) are used throughout for grid columns (Review.jsx's answer grid: grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4), spacing, and font sizes. ProgressBar.css and RadioSelector.css add small mobile-specific @media (max-width: 640px) overrides where Tailwind's responsive utilities alone weren't sufficient (repositioning the progress bar, stacking radio options vertically).
UI consistency¶
Buttonis used consistently as the single button primitive across every page.- The Likert-widget family (
RoundedLikert/SquaredLikert/SmileyLikert/Slider/CheckboxCard) shares a visual language (primary-green selectors, similar spacing) but each is a fully independent implementation with no shared base component — there is no generic<LikertOption>or<OptionList>abstraction underlying them (this compounds the duplication noted in §13).
15. Assets¶
| Asset type | Location | Notes |
|---|---|---|
| Bank/client logos | src/assets/diblogo.png, HRSGLogo.png, taj.png, mdow.png, mdow-alt.png |
Multiple client logos present in the same asset folder (DIB, HRSG, Martin Dow abbreviated "mdow") — confirms this codebase is reused/rebuilt per client engagement, with only a subset actively referenced by the current source (mdow-alt.png is imported by Language.jsx, Start.jsx, quiz.jsx, Review.jsx, FeedbackSubmission.jsx, SurveyCompletion.jsx — always imported but, in most of those files, only used inside a commented-out JSX block, i.e. imported-but-unrendered in the live UI). diblogo.png is imported by Layout/index.jsx but likewise only referenced inside a commented-out logo block (Layout/index.jsx:83-89) — the live header currently shows text only ("DIB Pakistan" / "Employee Engagement Survey"), no logo image. |
| Review-screen icon | src/assets/NoteBook.png |
Used by Review.jsx and FeedbackSubmission.jsx as a decorative header icon. |
| Urdu font | src/assets/fonts/JameelNooriNastaleeqRegular.ttf |
The only custom/bundled font file; declared via @font-face in index.css. |
| Vite default asset | src/assets/react.svg |
Unused scaffold leftover. |
| Public static images | public/background.png, public/bg-new.png, public/logo.png, public/logo-bg.jpg, public/logo-rm-bg.png |
Not referenced by any grep-able string in src/ — likely used only if public/survey-main.blade.php or a server-rendered shell references them directly (out of scope), or leftover from a prior build. |
| Likert option images | public/smileyLikert/1.png … 5.png |
Actively used by SmileyLikert.jsx (src="/smileyLikert/{value}.png"), one per Likert scale point. |
| Alternate smiley set | public/smileys/1.gif, 8.gif, 9.gif, 10.gif, 12.gif |
Referenced by filename arrays inside RoundedLikert.jsx, SquaredLikert.jsx, and Slider.jsx, but in each case the JSX block that would actually render them is commented out — so these GIFs are currently unused in the rendered UI, mirroring the mdow-alt.png situation above. |
| Runtime-fetched content | public/branding.json |
Not a binary asset but a static JSON file fetched at runtime by Start.jsx — see §6 and §12. |
| Legacy build snapshot | public/survey-main.blade.php |
A Laravel Blade template containing a different client's built <head> (title: "Martin Dow Employee Engagement Survey 2026", hashed asset filenames index-C7EXaJYr.js/index-DLBgWkL9.css). This is not part of the current app's build output or routing — it appears to be a leftover integration artifact (possibly for embedding the built SPA inside a Laravel-served page for a prior client) and is not referenced by vite.config.js, index.html, or any route. |
16. Environment Configuration¶
No .env file or import.meta.env.VITE_* usage¶
A full search of src/ finds no references to import.meta.env anywhere in the codebase. There is no .env, .env.example, or .env.* file in the project root. This means:
- The production API host is not configurable per environment via Vite env vars — it is hardcoded (see §9 and §22).
- There is no dev/staging/prod environment switch built into the app itself; switching backends would require editing src/api/axiosInstance.js:6 directly and rebuilding.
vite.config.js¶
Minimal configuration: binds the dev server to all network interfaces on port 5173 (useful for testing on other devices on the same network, e.g. a phone) and enables the official React plugin (Babel-based Fast Refresh). No path aliases, no build-time env injection, no manual chunking, no proxy configuration.
npm scripts (package.json:6-11)¶
Standard Vite scripts — no custom build variants (unlike a hypothetical per-client build script), which reinforces that per-client branding (see §15) is currently managed by swapping source files/assets between deployments rather than via build-time flags.
No TypeScript build step¶
Unlike its sibling "EES Report" frontend (per the task context, out of scope here), this app is plain JavaScript/JSX. @types/react and @types/react-dom are present in devDependencies (package.json:27-28) purely for editor IntelliSense on React's own types — there is no tsconfig.json, no .ts/.tsx file anywhere in src/, and vite.config.js uses the standard @vitejs/plugin-react (Babel), not @vitejs/plugin-react-swc or any TS-aware variant.
Deployment¶
Unable to determine from the available code — there is no CI config (no .github/workflows/, no buildspec.yml, no firebase.json, no Dockerfile) checked into this repository. The presence of public/survey-main.blade.php (see §15) hints at a Laravel-based hosting/embedding step for at least one prior deployment, but the mechanism connecting vite build's dist/ output to that Blade file is not present in this checkout.
17. Performance Optimizations¶
What exists¶
| Technique | Status |
|---|---|
React.memo |
Used once — SmileyLikert is wrapped in React.memo (src/components/LikertScale/SmileyLikert.jsx:79). No other component uses memo. |
| Framer Motion page-transition animation | quiz.jsx wraps its question content in a <motion.div> with initial/animate/exit opacity and a key={questionNo} (src/pages/quiz.jsx:197-206), producing a fade transition between questions. Note: since this isn't inside an AnimatePresence, the exit animation prop has no effect (there's no unmount transition, only mount). |
| Conditional rendering to avoid unnecessary fetches | Review.jsx's ReviewCard only re-fetches questions if questions.length === 0, avoiding a redundant call on the common path where Layout already populated the store. |
What is NOT present¶
| Technique | Status |
|---|---|
Route-level code splitting (React.lazy/Suspense) |
Not used — all pages/components are statically imported. |
useMemo/useCallback |
Not used anywhere in the codebase — every handler function and derived value is recreated on every render. |
| Debouncing | Not applicable (no search/typeahead inputs), but the free-text follow-up <textarea>s in CheckboxSelector/Slider dispatch/update state on every keystroke with no debounce, which is fine at this scale but worth noting for a future refactor. |
| Image optimization / lazy-loading | <img> tags have no loading="lazy" attribute; smiley/Likert images are small PNGs so this is a minor concern at current scale. |
| Bundle analysis / manual chunking | Not configured — Vite defaults only. |
Given the app's small size (52 source files, one route tree, no data tables or large lists beyond the Review grid), the absence of these optimizations is low-risk at current scale, but would matter if the question count or asset set grows significantly.
18. Error Handling¶
Layers¶
flowchart TB
A["API error"] --> B["Axios response interceptor: handleError"]
B --> C["Promise.reject with normalized message/details"]
C --> D{"Caller's try/catch"}
D -->|quiz.jsx, Language.jsx| E["console.error only - UI shows Swal warning for missing-answer case, not for the API error itself"]
D -->|FeedbackSubmission.jsx| F["Swal.fire error modal with Retry label"]
D -->|Layout/index.jsx| G["setIsError -> renders inline error text - instead of the page Outlet"]
D -->|questionsService submitSurvey/selectLanguage| H["handleError called AGAIN inside the try/catch - — redundant double-wrapping, see below"]
API error handling, call-site by call-site¶
Layout/index.jsx:55-58: the only place that surfaces the shape of a backend validation error to the user —error?.details?.errors?.[0] || "An unexpected error occurred."— implying the backend's error payload can include anerrorsarray (adetails.errors[0]message string), consistent withhandleError'sdetails: error?.response?.datanormalization. This rendered message replaces the entire page content (isErrorstate swaps out<Outlet/>for an<h1>).quiz.jsx(handleNext): onsubmitAnswerfailure, shows a generic SweetAlert2 "Submission Failed... Retry" modal — the actual error message fromhandleErroris not displayed, only logged viaconsole.error.Language.jsx(handleLanguageSelect):catchblock onlyconsole.errors — no user-visible feedback at all ifselectLanguagefails; thefinallyblock still clearsloading, so the UI silently returns to its normal state as if nothing happened.FeedbackSubmission.jsx(handleComplete): onsubmitSurveyfailure, shows a SweetAlert2 "Oops!... Retry" modal — again, generic copy, not the specific backend message.- Double error-handling in
questionsService.js:submitSurveyandselectLanguagebothcatch (error) { return handleError(error); }— buthandleErroris already the Axios response interceptor, so by the time thiscatchruns, the error has already been transformed once into{message, details}and rejected; callinghandleErroragain on that already-transformed object re-wraps it, producing{ message: transformedError.message ?? transformedError.message, details: transformedError }(since a{message, details}object has no.response,handleError's first two message sources areundefined, so it falls through toerror.message, which is present — so this happens to still produce a usable message, but only by coincidence ofhandleError's own fallback chain).fetchQuestionsandsubmitAnswer, by contrast, justthrow errorin theircatchblocks without callinghandleErrora second time — an inconsistency across the four service methods.
UI / render error fallback¶
- No global React error boundary anywhere in the codebase (no
componentDidCatch, noreact-error-boundarydependency, noErrorBoundarycomponent). An uncaught render-time exception in any component would blank the whole app (React 18's default behavior of unmounting the tree on an uncaught error in a class-less app with no boundary). - No catch-all route (see §4) — a stray/mistyped hash path silently renders an empty content area rather than a "not found" page.
Layout'sisErrorstate is the closest thing to a page-level fallback, but it only covers the initial questionnaire fetch, not any subsequent navigation or API call.
Logging¶
Exclusively console.log/console.error/console.warn calls scattered through the codebase (including at least one clearly-debug leftover, src/redux/slices/surveySlice.js:95's console.log(action.payload, "actions"), and src/services/questionsService.js:31's console.log(payload, "test")). No dedicated error-tracking SDK (e.g. Sentry) is configured — Unable to determine from the available code whether one is added at a hosting/infrastructure layer outside this repo.
19. Development Workflow¶
Setting up the project¶
cd "EES-2026/EES-Survey-Portal"
npm install
npm run dev # Vite dev server on http://0.0.0.0:5173 (per vite.config.js)
There is no .env to configure (see §16) — the app talks to the live production backend (https://dpak-ees-srv-26.engagesurvey.biz) from local dev by default, since the base URL is hardcoded. A local developer should be aware that running npm run dev and exercising the quiz flow will submit real API calls to production unless the backend team provides a way to redirect this, or the developer temporarily edits axiosInstance.js's baseURL.
Running the application¶
npm run dev— dev server with hot module reload.npm run build— production bundle todist/(default Vite output directory; not customized invite.config.js).npm run preview— serves the builtdist/bundle locally for a production-like smoke test.npm run lint— runs ESLint across the project pereslint.config.js.
Exercising the survey flow locally¶
Since there is no login, you need a valid qstr_url token to see anything past the "Invalid URL" message on Layout. Two ways to obtain one, both requiring the (out-of-scope) backend:
1. Navigate to http://localhost:5173/#/<valid-token> directly.
2. Manually set sessionStorage.setItem("qstr_url", "<valid-token>") in the browser console, then navigate to http://localhost:5173/#/.
Add a new page¶
- Create
src/pages/MyPage.jsx, export it as a default export. - Add it to the
src/pages/index.jsxbarrel. - Add a route entry under the
Layoutroute'schildrenarray insrc/routes/routes.jsx, wrapping in<StatusRoute>if it should be blocked once the survey is completed. - If it needs data from the store, read via
useSelector((state) => state.survey)— there is no separate per-page data-fetching hook pattern to follow beyond that.
Add a new component¶
- Create
src/components/MyComponent.jsx(orsrc/components/LikertScale/MyWidget.jsxif it's a new answer-type widget). - Export it from
src/components/index.jsxif it's meant to be broadly reusable (note several current components —Demographics,QuizTimer,FeedbackSubmission,SurveyCompletion— are not in the barrel and are imported by direct path instead; follow whichever convention matches the component's actual usage pattern). - If it's a new answer-widget type, also add a branch to
quiz.jsx'scurrentQuestion.questionTypeconditional chain (src/pages/quiz.jsx:208-364).
Connect to a new API endpoint¶
- Add a method to
src/services/questionsService.js(or a new sibling service file if it's a genuinely separate domain), following the existingtry/catch+apiClient.post(...)pattern. - Call it from the relevant page/component; dispatch any resulting state changes through the
surveyslice (adding new actions/reducers tosurveySlice.jsif needed).
20. Coding Standards¶
Naming conventions (as observed)¶
| Thing | Convention | Example |
|---|---|---|
| Component files | PascalCase .jsx |
Button.jsx, CheckboxSelector.jsx |
| One page file is lowercase | quiz.jsx (not Quiz.jsx) — inconsistent with every other page file (Language.jsx, Start.jsx, Review.jsx) |
src/pages/quiz.jsx |
| Service files | camelCase, *Service.js suffix |
questionsService.js |
| Redux slice files | camelCase, *Slice.js suffix |
surveySlice.js |
| Redux action/reducer names | camelCase, verb-first | setAnswer, setCurrentQuestionNo |
| CSS files | Co-located, same base name as the component | DropDown.jsx + DropDown.css |
| Props | camelCase | selectedOption, onSelect, quesData |
Import organization¶
- No path aliases — all imports are relative (
../components,../../redux/slices/surveySlice). This is consistent across the small codebase but would become unwieldy if the app grew deeper folder nesting. - Barrel exports exist for
pages/andcomponents/(top level only —LikertScale/is not separately barreled; its three files are re-exported individually fromcomponents/index.jsx).
Linting (eslint.config.js)¶
Flat-config ESLint 9 setup: js.configs.recommended + eslint-plugin-react's recommended and jsx-runtime rule sets + eslint-plugin-react-hooks's recommended rules + eslint-plugin-react-refresh. Two explicit overrides: react/jsx-no-target-blank is turned off, and react-refresh/only-export-components is downgraded to warn with allowConstantExport: true. No Prettier config file is present in the project — formatting is not enforced by tooling.
JS vs. TS tradeoffs (observed, not prescribed)¶
This app is plain JavaScript/JSX by choice or legacy (see §1, §16). Consequences visible in the code:
- Props are undocumented beyond usage (no PropTypes, no JSDoc @param blocks on components, though apiClient.js does use JSDoc comments for its four methods).
- Several small bugs that a type system would likely have caught at compile time are present and shipped, e.g. the setLoading reducer's no-op mutation (src/redux/slices/surveySlice.js:80-84, see §8 and §22) and the inconsistent error-return shapes across questionsService's four methods (see §18).
- The @types/react/@types/react-dom dev dependencies provide editor autocomplete for React's own API surface even without a TS build step, but do nothing for this app's own component prop shapes.
21. Debugging Guide¶
Tools¶
| Need | Tool |
|---|---|
| State inspection | Redux DevTools browser extension — works out of the box since configureStore enables it by default in development builds. Inspect state.survey directly (it's the only slice). |
| Network / API | Browser DevTools → Network tab. Watch requests to dpak-ees-srv-26.engagesurvey.biz/survey-api/*; confirm whether an Authorization header is present (it will be absent unless something has externally populated localStorage.authToken — see §11). |
| Component tree | React DevTools. |
| Session/token state | Browser DevTools → Application tab → Session Storage, check qstr_url and language keys directly. |
Common issues & where to look¶
| Symptom | Likely cause / where |
|---|---|
| "Invalid URL" shown immediately on load | Neither the :id route param nor sessionStorage.qstr_url is set. Check the URL hash includes a token (#/<token>), or manually seed sessionStorage. |
| Stuck on the loading spinner | fetchQuestions request is hanging or erroring silently before reaching the catch block's setIsError — check Network tab for the /survey-api/get-questionnaire call's status. |
Redirected to /completion unexpectedly |
state.survey.status === "completed" — this comes directly from the backend's qstr.status field on the last fetchQuestions response; not a frontend bug unless the backend is returning stale status. |
| Answer doesn't seem to save on refresh | Confirm the submitAnswer POST actually fired (Network tab) — setAnswer alone only updates local Redux state; only clicking Next/Submit/Back-to-Review triggers the API call. |
| Loading indicator toggle not working as expected | setLoading reducer is a no-op due to the state = tempState bug (see §8) — do not rely on dispatch(setLoading(...)) having any effect; Layout's own local loading/setLocalLoading useState is what actually drives its spinner. |
| Urdu text not rendering in the custom font | Confirm the element has the inline fontFamily: "Jameel Noori Nastaleeq" style applied directly — there is no global class or CSS selector that applies it automatically based on lang/dir. |
| A component you expect to see doesn't render | Check whether it's one of the four dead components (Demographics, QuizTimer, Modal, Accordion) — confirm it's actually imported somewhere in the current route/component tree before debugging further. |
API debugging tips¶
- All four
questionsServicemethods log either the payload or the response viaconsole.logbefore/after the call — check the browser console alongside Network tab. - The backend's success response for
fetchQuestionsis expected to be wrapped as{ res: { qstr, questions, scales } }— ifresponse.resisundefined,Layout'sif (response?.res)check will silently skip both dispatches and just log a warning (src/Layout/index.jsx:52-54), leaving the store in its default empty state without an explicit error being shown to the user.
22. Common Pitfalls¶
| Pitfall | Why it happens | Do this instead |
|---|---|---|
| Hardcoded production API host committed to source | src/api/axiosInstance.js:6 — baseURL: "https://dpak-ees-srv-26.engagesurvey.biz" is a literal string, not read from import.meta.env. Every environment (local dev, staging, preview) talks to the same live production backend. |
Introduce a Vite env var (VITE_API_BASE_URL) and fall back to this value only for local convenience; never assume npm run dev is safe to point at a non-production API without deliberate configuration. |
setLoading reducer is a silent no-op |
src/redux/slices/surveySlice.js:80-84 mutates a local tempState copy and reassigns the state function parameter rather than mutating the Immer draft or returning a new object — Redux Toolkit's Immer wrapper never sees this change. |
Rewrite as state.loading = action.payload; (mutate the draft directly, consistent with every other reducer in this slice), or return { ...state, loading: action.payload };. |
:id route (the actual invite-link shape) bypasses StatusRoute |
src/routes/routes.jsx:21-24 — /:id renders Language directly, with no StatusRoute wrapper, unlike the / index route. An already-completed survey's original invite link, if reopened, will show the Language picker again (briefly) before the user clicks "Continue," rather than being redirected to /completion immediately the way / is. |
Wrap the /:id route's Language element in <StatusRoute> the same way the index route is, for consistent behavior regardless of which URL shape the employee's link uses. |
Two dead selector-map lookups (Review.jsx) can silently show a blank answer |
src/pages/Review.jsx:168-171 matches the displayed answer purely by option.value === answers[i].score; for question types where score doesn't correspond to a listed option.value (e.g. certain "integrity"/free-text-heavy answers), answer[0] will be undefined and the card renders nothing where a label is expected. |
Add a fallback display (e.g. show other_text or a generic "answered" indicator) when no matching option is found. |
authToken is read but never written |
src/api/axiosInstance.js:13 reads localStorage.getItem("authToken"), but no code path in this repo ever calls localStorage.setItem("authToken", ...). |
Either remove the dead interceptor logic if truly unused, or confirm with the backend team whether a token is meant to be issued and persisted somewhere in the flow (e.g. from select-language or get-questionnaire's response) and wire that up. |
Redundant double error-wrapping in questionsService |
submitSurvey/selectLanguage call handleError(error) inside their own catch, but handleError is also already the Axios response interceptor — the error has already been transformed once by the time the service's catch runs. |
Standardize all four service methods on one pattern: either let the interceptor be the single source of error normalization (just throw error; or don't catch at all), or remove the interceptor's error handling and do it uniformly at the service layer — don't do both. |
No catch-all (*) route |
src/routes/routes.jsx has no fallback route entry. |
Add a path: "*" child route rendering a simple "not found" or redirect-to-/ element. |
i18n.js is fully configured but never imported/initialized |
No file imports src/i18n.js; react-i18next's useTranslation() is consequently unused everywhere, and the app instead hand-rolls language switching via direct JSON-object imports (see §12). |
Either delete i18n.js and the i18next/react-i18next dependencies if the manual pattern is intentional and permanent, or actually wire it in and migrate the manual translations.foo call sites to t("foo") for consistency and to gain i18next's pluralization/interpolation features. |
| Extensive commented-out code left in shipped source | Slider.jsx:1-227 (an entire prior implementation), and commented-out "powered by Engage Consulting" footer / logo blocks repeated near-verbatim across Start.jsx, quiz.jsx, Review.jsx, FeedbackSubmission.jsx, SurveyCompletion.jsx, Layout/index.jsx. |
Remove dead code before merging; rely on git history to recover prior implementations rather than commenting them out in place. |
Four fully-authored-but-unused components (Demographics, QuizTimer, Modal, Accordion) |
Built but never imported into any route/page — see §7. | Confirm with product ownership whether these are planned (in which case, wire them in and keep them current) or abandoned (in which case, remove them to avoid confusing future contributors into thinking demographics collection or a quiz timer is live functionality). |
23. Best Practices¶
Components¶
- Keep the existing pattern of "one file per component, default export" — it's simple and consistent across this small codebase.
- When adding a new Likert/answer-type widget, follow the existing prop contract (
quesData,selectedOption,onSelect) so it slots cleanly intoquiz.jsx's question-type switch without special-casing. - Before extending
Demographics,QuizTimer,Modal, orAccordion, confirm they're actually intended to ship — see §22.
State management¶
- Continue funneling all survey data through the single
surveyslice rather than introducing component-local duplicates of server state. - Be aware that
setLoadingis currently broken (see §22) — don't build new functionality on the assumption that dispatching it has any effect until it's fixed. - Prefer adding new reducers/actions to
surveySlice.jsover introducing a second slice, unless a genuinely independent domain (e.g. a future demographics feature) justifies its own slice androotReducer.jsentry.
API integration¶
- Keep new backend calls behind
questionsService.js(or a clearly-named sibling service file) — never callaxiosInstance/apiClientdirectly from a component. - Standardize error handling across any new service methods (see the double-wrapping pitfall above) rather than propagating the current inconsistency.
- Treat the backend response envelope's
reswrapper (response.data.res) as the established convention when adding new endpoints, unless the backend team confirms otherwise.
Performance¶
- The app is small enough that
React.lazy/route-level splitting is not urgent, but would be a reasonable addition if more pages are added. - Wrap
AnimatePresencearoundquiz.jsx'smotion.divif an exit transition between questions is actually desired — currently theexitprop has no effect without it.
Maintainability¶
- Extract the repeated Urdu/RTL inline-style objects and the repeated
ButtonStyles/SweetAlert2 confirm-button-recolor callback into shared helpers (asrc/utils/folder does not exist yet — this would be the natural place to start one). - Remove commented-out code blocks (see §22) rather than accumulating them further.
- If multi-client/white-label deployment (DIB, Martin Dow, etc.) is an ongoing pattern, consider centralizing per-client config (title, bank name, logo, primary color) into a single config file or env-driven mechanism rather than editing source strings per deployment — this would also resolve the hardcoded strings noted in
Start.jsx,Layout/index.jsx, andindex.html.
Accessibility¶
- Current state: no systematic accessibility patterns found — custom checkbox/radio/dropdown components use
<div>/<span>withonClickhandlers and hidden native inputs rather than fully accessible custom-control patterns; noaria-*attributes were found in any component. Unable to determine from the available code whether accessibility was explicitly deprioritized or simply not yet addressed. - For new work: add keyboard interaction and
aria-*labeling to the custom selector components (RadioSelector,CheckboxSelector,DropDown) before they see wider use, since they currently rely entirely on mouse/touchonClick.
Code reuse¶
- Establish
src/hooks/andsrc/utils/folders as this app grows — the current "everything inline" pattern is tenable at 52 files but will not scale gracefully.
24. Developer Onboarding Checklist¶
Work through this in order. Check each box.
Environment setup¶
- Clone/open the repo at
EES-2026/EES-Survey-Portal. - Read this document's §1–§5 in full.
-
npm install. -
npm run devand openhttp://localhost:5173. - Understand that the dev server talks to the live production backend by default (no
.envswitch exists) — see §16 and §22.
Understand the project structure¶
- Skim
src/and match each folder to §2. - Read
src/main.jsx,src/routes/routes.jsx, andsrc/Layout/index.jsx— this is the entire bootstrap path from page load to a rendered survey screen (§3). - Read
src/redux/slices/surveySlice.jsfully — it is the single source of truth for the whole app's data model (§8).
Run & explore¶
- Obtain (or synthesize, per §19) a valid
qstr_urltoken and walk the full flow: Language → Start → Quiz (all question types) → Review → Submit → Completion. - Open Redux DevTools; watch
state.survey.questionNo/answersupdate as you progress through the quiz. - Open the Network tab; confirm the four
/survey-api/*calls fire at the expected points (§5, §9).
Learn routing & state¶
- Read
src/routes/StatusRoute.jsxand understand the completion-redirect guard, and its one gap (the/:idroute bypasses it) — §4, §22. - Trace one full answer submission from
quiz.jsx'shandleAnswer/handleNextthroughsetAnswer/setCurrentQuestionNo— §5, §8.
Learn the component set¶
- Read all five answer widgets (
RoundedLikert,SquaredLikert,SmileyLikert,Slider,CheckboxCard/CheckboxSelector) and howquiz.jsxselects between them byquestionType— §7. - Note the four dead components (
Demographics,QuizTimer,Modal,Accordion) and confirm their status with the team before building on or deleting them.
Learn i18n¶
- Read §12 and understand that i18next is configured but inert — new bilingual UI copy should follow the existing
translations.foo(locale JSON import) pattern, notuseTranslation(), unless the team decides to migrate.
Make your first change¶
- Add a harmless UI tweak (e.g., a new locale string used in an existing page) and confirm it appears correctly in both English and Urdu.
- Run
npm run lintand confirm it passes before proposing a change.
Submit a change¶
- Confirm whether the current git/PR workflow for this repo is defined elsewhere — Unable to determine from the available code (no CI config, branch protection rules, or CONTRIBUTING doc found in this checkout).
25. Appendix — "Unable to determine from the available code"¶
- The
/survey-api/*backend is entirely out of scope for this documentation pass. Its source code does not exist in this checkout. Everything in §9 describing request payloads and response usage is inferred solely from the frontend's call sites (src/services/questionsService.js) — actual validation rules, authentication requirements, response schemas beyond the fields this frontend happens to read, rate limiting, and token/link expiry behavior are all unknown and must be documented separately once that codebase is available. - Whether the
authToken/localStorageBearer-token mechanism inaxiosInstance.jsis dead scaffolding or a partially-implemented feature awaiting a write-side (see §11, §22). - Whether
Demographics.jsx,QuizTimer.jsx,Modal.jsx, andAccordion.jsxare planned features awaiting integration or abandoned code (see §7, §22). - The exact mechanism and ownership behind
public/survey-main.blade.php— which client/deployment it belongs to, whether it's still in active use, and how (if at all) it relates to this Vite app's own build/deploy pipeline (see §15, §16). - Deployment/CI details: no
.github/workflows/,Dockerfile,buildspec.yml, or hosting configuration exists in this checkout. - The team's branch/PR review policy — no CONTRIBUTING file or CI gate found.
- Whether a client-side error-tracking SDK (e.g., Sentry) is wired in at a layer outside this repository (e.g., injected at hosting/CDN level).
- The full list of clients this codebase has been white-labeled for beyond what's inferable from asset filenames (
diblogo,HRSGLogo,taj,mdow/mdow-alt) and the Martin Dow reference insurvey-main.blade.php. - Why
package.json'snamefield ("Jubilee-survey") and the Redux slice's internalname("Jubilee-survey",src/redux/slices/surveySlice.js:17) don't match the DIB/EES branding shown to end users — likely an internal/original project codename, but its origin is not documented in the code itself.
This document is derived from static analysis of the source at d:/Downloads/EES Documentation/EES-2026/EES-Survey-Portal as of the analysis date. Line references reflect the code at analysis time and may drift as the app changes.