update to use auditly-consulting firebase, gemini, fix settings/help pages to be their own pages (upload image not yet functional).

This commit is contained in:
Ra
2025-09-02 13:32:48 -07:00
parent 2e15a74c76
commit 899900f99c
24 changed files with 1480 additions and 5674 deletions

3
.gitignore vendored
View File

@@ -65,4 +65,5 @@ dist-ssr
/deploy-security.sh
/EMPLOYEE_FORMS_FIGMA_README.md
/TODOS.md
/SECURITY_MIGRATION.md
/SECURITY_MIGRATION.md
/employee_report_schema.json

View File

@@ -1,161 +0,0 @@
{
"type": "object",
"properties": {
"employeeId": {
"type": "string"
},
"department": {
"type": "string"
},
"role": {
"type": "string"
},
"roleAndOutput": {
"type": "object",
"properties": {
"responsibilities": {
"type": "string",
"examples": [
"Recruiting influencers, onboarding, campaign support, business development."
]
},
"clarityOnRole": {
"type": "string",
"examples": [
"10/10 Feels very clear on responsibilities."
]
},
"selfRatedOutput": {
"type": "string",
"examples": [
"7/10 Indicates decent performance but room to grow."
]
},
"recurringTasks": {
"type": "string",
"examples": [
"Influencer outreach, onboarding, communications."
]
}
}
},
"insights": {
"type": "object",
"properties": {
"personalityInsights": {
"type": "string",
"examples": [
"Loyal, well-liked by influencers, eager to grow, client-facing interest."
]
},
"psychologicalIndicators": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"Scores high on optimism and external motivation.",
"Shows ambition but lacks self-discipline in execution.",
"Displays a desire for recognition and community; seeks more appreciation."
]
]
},
"selfAwareness": {
"type": "string",
"examples": [
"High acknowledges weaknesses like lateness and disorganization."
]
},
"emotionalResponses": {
"type": "string",
"examples": [
"Frustrated by campaign disorganization; would prefer closer collaboration."
]
},
"growthDesire": {
"type": "string",
"examples": [
"Interested in becoming more client-facing and shifting toward biz dev."
]
}
}
},
"strengths": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"Builds strong relationships with influencers.",
"Has sales and outreach potential.",
"Loyal, driven, and values-aligned with the company mission.",
"Open to feedback and self-improvement."
]
]
},
"weaknessess": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
"Critical Issue: Disorganized and late with deliverables — confirmed by previous internal notes.",
"Poor implementation and recruiting output — does not effectively close the loop on influencer onboarding.",
"May unintentionally cause friction with campaigns team by stepping outside process boundaries."
]
},
"opportunities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
}
}
},
"examples": [
[
{
"title": "Role Adjustment",
"description": "Shift fully to Influencer Manager & Biz Dev Outreach as planned. Remove all execution and recruitment responsibilities."
},
{
"title": "Accountability Support",
"description": "Pair with a high-output implementer (new hire) to balance Gentrys strategic skills."
}
]
]
},
"risks": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"Without strict structure, Gentrys performance will stay flat or become a bottleneck.",
"If kept in a dual-role (recruiting + outreach), productivity will suffer.",
"He needs system constraints and direct oversight to stay focused."
]
]
},
"recommendations": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"Keep. But immediately restructure his role: Remove recruiting and logistical tasks. Focus only on influencer relationship-building, pitching, and business development.",
"Pair him with a new hire who is ultra-organized and can execute on Gentrys deals."
]
]
}
}
}

View File

@@ -4,9 +4,9 @@
"": {
"name": "auditly-functions",
"dependencies": {
"@google-cloud/vertexai": "^1.4.0",
"firebase-admin": "^12.1.1",
"firebase-functions": "^5.0.1",
"openai": "^5.12.2",
"stripe": "^18.4.0",
},
"devDependencies": {
@@ -123,6 +123,8 @@
"@google-cloud/storage": ["@google-cloud/storage@7.16.0", "", { "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", "@google-cloud/promisify": "<4.1.0", "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", "fast-xml-parser": "^4.4.1", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", "mime": "^3.0.0", "p-limit": "^3.0.1", "retry-request": "^7.0.0", "teeny-request": "^9.0.0", "uuid": "^8.0.0" } }, "sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw=="],
"@google-cloud/vertexai": ["@google-cloud/vertexai@1.10.0", "", { "dependencies": { "google-auth-library": "^9.1.0" } }, "sha512-HqYqoivNtkq59po8m7KI0n+lWKdz4kabENncYQXZCX/hBWJfXtKAfR/2nUQsP+TwSfHKoA7zDL2RrJYIv/j3VQ=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="],
"@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
@@ -677,11 +679,11 @@
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jwks-rsa": ["jwks-rsa@3.2.0", "", { "dependencies": { "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", "jose": "^4.15.4", "limiter": "^1.1.5", "lru-memoizer": "^2.2.0" } }, "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww=="],
"jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
"jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
@@ -777,8 +779,6 @@
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"openai": ["openai@5.12.2", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
@@ -1007,16 +1007,14 @@
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"google-auth-library/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
"google-gax/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"gtoken/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
"http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
"jwks-rsa/@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -1055,9 +1053,7 @@
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"google-auth-library/jws/jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"gtoken/jws/jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jsonwebtoken/jws/jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@
"dependencies": {
"firebase-admin": "^12.1.1",
"firebase-functions": "^5.0.1",
"openai": "^5.12.2",
"@google-cloud/vertexai": "^1.4.0",
"stripe": "^18.4.0"
},
"devDependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

View File

@@ -11,17 +11,14 @@ const CompanyWiki = React.lazy(() => import('./pages/CompanyWiki'));
const Reports = React.lazy(() => import('./pages/Reports'));
const Submissions = React.lazy(() => import('./pages/Submissions'));
const Chat = React.lazy(() => import('./pages/Chat'));
const HelpNew = React.lazy(() => import('./pages/HelpNew'));
const SettingsNew = React.lazy(() => import('./pages/SettingsNew'));
const HelpNew = React.lazy(() => import('./pages/Help'));
const SettingsNew = React.lazy(() => import('./pages/Settings'));
const ModernLogin = React.lazy(() => import('./pages/Login'));
const OrgSelection = React.lazy(() => import('./pages/OrgSelection'));
const Onboarding = React.lazy(() => import('./pages/Onboarding'));
const EmployeeQuestionnaire = React.lazy(() => import('./pages/EmployeeQuestionnaire'));
const EmployeeQuestionnaireNew = React.lazy(() => import('./pages/EmployeeQuestionnaireNew'));
const EmployeeQuestionnaireSteps = React.lazy(() => import('./pages/EmployeeQuestionnaireSteps'));
const EmployeeQuestionnaireNew = React.lazy(() => import('./pages/EmployeeQuestionnaire'));
const QuestionTypesDemo = React.lazy(() => import('./pages/QuestionTypesDemo'));
const FormsDashboard = React.lazy(() => import('./pages/FormsDashboard'));
const QuestionnaireComplete = React.lazy(() => import('./pages/QuestionnaireComplete'));
const SubscriptionSetup = React.lazy(() => import('./pages/SubscriptionSetup'));
// Loading component for Suspense fallback
@@ -123,15 +120,11 @@ function App() {
<Routes>
<Route path="/login" element={<SuspenseWrapper><ModernLogin /></SuspenseWrapper>} />
<Route path="/login/:inviteCode" element={<SuspenseWrapper><ModernLogin /></SuspenseWrapper>} />
{/* <Route path="/invite/:inviteCode" element={<InviteRedirect />} /> */}
{/* Employee questionnaire - no auth needed, uses invite code */}
<Route path="/employee-form/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>} />
<Route path="/questionnaire/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>} />
{/* Legacy employee questionnaire route for backwards compatibility */}
<Route path="/employee-form-legacy/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaire /></SuspenseWrapper>} />
{/* Organization Selection - after auth, before entering app */}
<Route
path="/org-selection"
@@ -166,33 +159,6 @@ function App() {
}
/>
{/* Legacy employee questionnaire route for backwards compatibility */}
<Route
path="/employee-questionnaire-legacy"
element={
<RequireAuth>
<RequireOrgSelection>
<OrgProviderWrapper>
<SuspenseWrapper><EmployeeQuestionnaire /></SuspenseWrapper>
</OrgProviderWrapper>
</RequireOrgSelection>
</RequireAuth>
}
/>
<Route
path="/employee-questionnaire-steps"
element={
<RequireAuth>
<RequireOrgSelection>
<OrgProviderWrapper>
<SuspenseWrapper><EmployeeQuestionnaireSteps /></SuspenseWrapper>
</OrgProviderWrapper>
</RequireOrgSelection>
</RequireAuth>
}
/>
<Route
path="/onboarding"
element={
@@ -206,8 +172,6 @@ function App() {
}
/>
<Route path="/questionnaire-complete" element={<SuspenseWrapper><QuestionnaireComplete /></SuspenseWrapper>} />
{/* New Figma Chat Implementation - Standalone route */}
<Route
path="/chat"
@@ -223,7 +187,6 @@ function App() {
</RequireAuth>
}
/>
{/* New Figma Help Implementation - Standalone route */}
<Route
path="/help"
@@ -274,8 +237,6 @@ function App() {
<Route path="/company-wiki" element={<SuspenseWrapper><CompanyWiki /></SuspenseWrapper>} />
<Route path="/submissions" element={<SuspenseWrapper><Submissions /></SuspenseWrapper>} />
<Route path="/reports" element={<SuspenseWrapper><Reports /></SuspenseWrapper>} />
{/* <Route path="/help" element={<SuspenseWrapper><HelpNew /></SuspenseWrapper>} />
<Route path="/settings" element={<SuspenseWrapper><SettingsNew /></SuspenseWrapper>} /> */}
</Route>
{/* Debug routes */}

View File

@@ -510,7 +510,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
>
<div className="relative">
{React.cloneElement(settingsIcon, {
stroke: location.pathname === "/settings" ? "var(--Brand-Orange)" : "var(--Neutrals-NeutralSlate400, #A4A7AE)"
stroke: location.pathname === "/settings" ? "var(--Brand-Orange)" : "var(--Neutrals-NeutralSlate400)"
})}
</div>
<div className={`flex-1 justify-start text-sm font-medium font-['Inter'] leading-tight ${location.pathname === "/settings"

View File

@@ -7,16 +7,26 @@ export const SITE_URL = import.meta.env.VITE_SITE_URL || 'http://localhost:5173'
const isLocalhost = typeof window !== 'undefined' &&
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
// Check if we should force production API (useful for testing invite links locally)
const forceProductionAPI = typeof window !== 'undefined' &&
window.location.search.includes('use-production-api=true');
// Use Firebase Functions emulator in development, production functions in production
export const API_URL = isLocalhost
? 'http://127.0.0.1:5002/auditly-c0027/us-central1' // Firebase Functions Emulator
: 'https://us-central1-auditly-c0027.cloudfunctions.net'; // Production Firebase Functions
// export const API_URL = (isLocalhost && !forceProductionAPI)
// ? 'http://127.0.0.1:5002/auditly-consulting/us-central1' // Firebase Functions Emulator
// : 'https://us-central1-auditly-consulting.cloudfunctions.net'; // Production Firebase Functions
// export const API_URL = 'https://us-central1-auditly-c0027.cloudfunctions.net';
export const API_URL = 'https://us-central1-auditly-consulting.cloudfunctions.net';
// export const API_URL = 'http://127.0.0.1:5002/auditly-consulting/us-central1';
// Log URL configuration in development
if (import.meta.env.DEV) {
console.log('🌐 Frontend URL Configuration:');
console.log(` SITE_URL: ${SITE_URL}`);
console.log(` API_URL: ${API_URL}`);
console.log(` isLocalhost: ${isLocalhost}`);
console.log(` forceProductionAPI: ${forceProductionAPI}`);
}
// DEPRECATED: These are legacy sample data that should not be used in production
@@ -43,21 +53,25 @@ export const REPORT_DATA: EmployeeReport = {
},
strengths: ['Sample data - use AI-generated reports'],
weaknesses: [
{ isCritical: false, description: 'Sample data - use AI-generated reports' }
'Sample data - use AI-generated reports'
],
opportunities: [
{
roleAdjustment: 'Sample data',
accountabilitySupport: 'Sample data',
title: 'Sample data',
description: 'Sample data',
}
],
risks: ['Sample data - use AI-generated reports'],
recommendations: ['Sample recommendation - use AI-generated reports'],
recommendation: {
action: 'Keep',
details: ['Sample data - use AI-generated reports'],
},
grading: [],
gradingOverview: {
employeeName: 'Sample Employee',
scalability: 3,
reliability: 4,
roleFit: 3,
output: 3,
grade: "A",
initiative: 10
}
};
// DEPRECATED: Sample submission data - real submissions come from employee questionnaires
@@ -253,12 +267,6 @@ export const SAMPLE_COMPANY_REPORT: CompanyReport = {
{ department: 'Tech', role: 'Frontend Developer', priority: 'High', reasoning: 'Need to expand web development capacity for growing client demands' },
{ department: 'Social Media', role: 'Content Creator', priority: 'Medium', reasoning: 'Additional support needed for video content production' }
],
recommendations: [
'Hire additional frontend developer for tech team expansion',
'Implement cross-departmental collaboration sessions',
'Develop leadership training program for senior staff',
'Establish mentorship program for junior employees'
],
forwardOperatingPlan: [
{
title: 'Q1 Client Growth Initiative',

View File

@@ -1,5 +1,5 @@
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
import { signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
import { API_URL } from '../constants';
@@ -23,54 +23,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
useEffect(() => {
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
if (isFirebaseConfigured) {
// Firebase mode: Set up proper Firebase auth state listener
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
console.log('Firebase auth state changed:', firebaseUser?.email);
if (firebaseUser) {
setUser(firebaseUser);
} else {
// Check for OTP session as fallback
const sessionUser = localStorage.getItem('auditly_demo_session');
if (sessionUser) {
try {
const parsedUser = JSON.parse(sessionUser);
console.log('Restoring OTP session for:', parsedUser.email);
setUser(parsedUser as User);
} catch (error) {
console.error('Failed to parse session user:', error);
localStorage.removeItem('auditly_demo_session');
setUser(null);
}
} else {
setUser(null);
}
}
setLoading(false);
});
return unsubscribe;
} else {
// Demo/OTP mode: Check localStorage for persisted session
console.log('Checking for persisted OTP session');
const sessionUser = localStorage.getItem('auditly_demo_session');
if (sessionUser) {
try {
const parsedUser = JSON.parse(sessionUser);
console.log('Restoring session for:', parsedUser.email);
setUser(parsedUser as User);
} catch (error) {
console.error('Failed to parse session user:', error);
localStorage.removeItem('auditly_demo_session');
setUser(null);
}
} else {
// Demo/OTP mode: Check localStorage for persisted session
console.log('Checking for persisted OTP session');
const sessionUser = localStorage.getItem('auditly_demo_session');
if (sessionUser) {
try {
const parsedUser = JSON.parse(sessionUser);
console.log('Restoring session for:', parsedUser.email);
setUser(parsedUser as User);
} catch (error) {
console.error('Failed to parse session user:', error);
localStorage.removeItem('auditly_demo_session');
setUser(null);
}
setLoading(false);
return () => { };
} else {
setUser(null);
}
setLoading(false);
return () => { };
}, []);
const signInWithGoogle = useCallback(async () => {

View File

@@ -1,7 +1,5 @@
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
import { useAuth } from './AuthContext';
import { isFirebaseConfigured } from '../services/firebase';
import { API_URL } from '../constants';
import { secureApi } from '../services/secureApi';
interface UserOrganization {

File diff suppressed because it is too large Load Diff

View File

@@ -1,600 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { EmployeeSubmissionAnswers } from '../employeeQuestions';
import { API_URL } from '../constants';
import {
WelcomeScreen,
SectionIntro,
PersonalInfoForm,
TextAreaQuestion,
RatingScaleQuestion,
YesNoChoice,
ThankYouPage
} from '../components/figma/FigmaEmployeeForms';
/**
* Employee Questionnaire with Invite-Only Flow
*
* Features:
* - Invite-based flow (no authentication required)
* - Company owner invites employees with metadata
* - Employee uses invite code to access questionnaire
* - LLM processing via cloud functions with company context
* - Report generation and Firestore storage
*/
const EmployeeQuestionnaire: React.FC = () => {
const params = useParams();
const inviteCode = params.inviteCode;
// Component state
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<any>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [inviteEmployee, setInviteEmployee] = useState<any>(null);
const [isLoadingInvite, setIsLoadingInvite] = useState(false);
// Load invite details on component mount
useEffect(() => {
if (inviteCode) {
loadInviteDetails(inviteCode);
} else {
setError('No invite code provided');
}
}, [inviteCode]);
const loadInviteDetails = async (code: string) => {
setIsLoadingInvite(true);
try {
const response = await fetch(`${API_URL}/getInvitationStatus?code=${code}`);
if (response.ok) {
const data = await response.json();
if (data.used) {
setError('This invitation has already been used');
} else if (data.employee) {
setInviteEmployee(data.employee);
// Pre-populate form data with invite metadata
setFormData({
name: data.employee.name || '',
email: data.employee.email || '',
company: data.employee.company || data.employee.department || ''
});
setError('');
} else {
setError('Invalid invitation data');
}
} else {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
setError(errorData.error || 'Invalid or expired invitation link');
}
} catch (err) {
console.error('Error loading invite details:', err);
setError('Failed to load invitation details');
} finally {
setIsLoadingInvite(false);
}
};
// Get employee info from invite data
const currentEmployee = inviteEmployee;
const submitViaInvite = async (answers: EmployeeSubmissionAnswers, inviteCode: string) => {
try {
// Submit the questionnaire answers directly using Cloud Function
// The cloud function will handle invite consumption and report generation
const submitResponse = await fetch(`${API_URL}/submitEmployeeAnswers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
inviteCode: inviteCode,
answers: answers
})
});
if (!submitResponse.ok) {
const errorData = await submitResponse.json();
throw new Error(errorData.error || 'Failed to submit questionnaire');
}
const result = await submitResponse.json();
return { success: true, reportGenerated: true };
} catch (error) {
console.error('Invite submission error:', error);
return { success: false, error: error.message };
}
};
const handleSubmit = async () => {
setIsSubmitting(true);
setError('');
try {
// Convert form data to answers format for backend
const answers: EmployeeSubmissionAnswers = {};
// Map form data to question IDs
if (formData.name) answers['full_name'] = formData.name;
if (formData.email) answers['email'] = formData.email;
if (formData.company) answers['company_department'] = formData.company;
// Add all other form data fields
Object.keys(formData).forEach(key => {
if (formData[key] && !answers[key]) {
answers[key] = formData[key];
}
});
// Submit answers via invite flow
const result = await submitViaInvite(answers, inviteCode!);
if (result.success) {
// Show thank you page
setCurrentStep(999);
} else {
setError(result.error || 'Failed to submit questionnaire');
}
} catch (error) {
console.error('Submission error:', error);
setError('Failed to submit questionnaire. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const handleNext = (stepData?: any) => {
if (stepData) {
const newFormData = { ...formData, ...stepData };
setFormData(newFormData);
}
setCurrentStep(currentStep + 1);
};
const handleBack = () => {
setCurrentStep(currentStep - 1);
};
// Early return for loading state
if (isLoadingInvite) {
return (
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
<div className="max-w-4xl mx-auto text-center">
<div className="w-16 h-16 bg-[--Brand-Orange] rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
A
</div>
<h1 className="text-3xl font-bold text-[--Neutrals-NeutralSlate950] mb-4">Loading Your Invitation...</h1>
<p className="text-[--Neutrals-NeutralSlate500]">Please wait while we verify your invitation.</p>
</div>
</div>
);
}
// Early return for error state
if (error && currentStep === 1) {
return (
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
<div className="max-w-4xl mx-auto text-center">
<div className="w-16 h-16 bg-red-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
!
</div>
<h1 className="text-3xl font-bold text-[--Neutrals-NeutralSlate950] mb-4">Invitation Error</h1>
<p className="text-[--Neutrals-NeutralSlate500] mb-6">{error}</p>
<button
onClick={() => window.location.href = '/'}
className="px-6 py-3 bg-[--Brand-Orange] text-white rounded-lg hover:bg-orange-600"
>
Return to Homepage
</button>
</div>
</div>
);
}
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<WelcomeScreen
onStart={() => handleNext()}
/>
);
case 2:
return (
<PersonalInfoForm
formData={{
email: formData.email || '',
name: formData.name || '',
company: formData.company || ''
}}
onChange={(data) => setFormData({ ...formData, ...data })}
onNext={() => handleNext()}
/>
);
case 3:
return (
<SectionIntro
sectionNumber="1 of 6"
title="Your Role & Responsibilities"
description="Let's start by understanding your current role and daily responsibilities."
onStart={() => handleNext()}
/>
);
case 4:
return (
<TextAreaQuestion
question="What is your current title and department?"
value={formData.titleAndDepartment || ''}
onChange={(value) => setFormData({ ...formData, titleAndDepartment: value })}
onNext={() => handleNext()}
currentStep={1}
totalSteps={7}
sectionName="Your Role & Responsibilities"
/>
);
case 5:
return (
<TextAreaQuestion
question="Describe your core daily responsibilities"
value={formData.dailyResponsibilities || ''}
onChange={(value) => setFormData({ ...formData, dailyResponsibilities: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={2}
totalSteps={7}
sectionName="Your Role & Responsibilities"
placeholder="Describe what you do on a typical day..."
/>
);
case 6:
return (
<RatingScaleQuestion
question="How clearly do you understand your role and responsibilities?"
leftLabel="Not clear"
rightLabel="Very clear"
value={formData.roleClarity}
onChange={(value) => setFormData({ ...formData, roleClarity: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={3}
totalSteps={7}
sectionName="Your Role & Responsibilities"
scale={10}
/>
);
case 7:
return (
<SectionIntro
sectionNumber="2 of 6"
title="Output & Accountability"
description="Let's explore your work output, goals, and accountability measures."
onStart={() => handleNext()}
/>
);
case 8:
return (
<RatingScaleQuestion
question="How would you rate your weekly output (volume & quality)?"
leftLabel="Very little"
rightLabel="Very High"
value={formData.weeklyOutput}
onChange={(value) => setFormData({ ...formData, weeklyOutput: value })}
onNext={() => handleNext()}
currentStep={1}
totalSteps={7}
sectionName="Output & Accountability"
scale={10}
/>
);
case 9:
return (
<TextAreaQuestion
question="What are your top 23 recurring deliverables?"
value={formData.topDeliverables || ''}
onChange={(value) => setFormData({ ...formData, topDeliverables: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={2}
totalSteps={7}
sectionName="Output & Accountability"
/>
);
case 10:
return (
<YesNoChoice
question="Do you have weekly KPIs or goals?"
value={formData.hasKPIs}
onChange={(value) => setFormData({ ...formData, hasKPIs: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={3}
totalSteps={7}
sectionName="Output & Accountability"
/>
);
case 11:
return (
<TextAreaQuestion
question="Who do you report to? How often do you meet/check-in?"
value={formData.reportingStructure || ''}
onChange={(value) => setFormData({ ...formData, reportingStructure: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={4}
totalSteps={7}
sectionName="Output & Accountability"
/>
);
case 12:
return (
<SectionIntro
sectionNumber="3 of 6"
title="Team & Collaboration"
description="Let's understand your team dynamics and collaboration patterns."
onStart={() => handleNext()}
/>
);
case 13:
return (
<TextAreaQuestion
question="Who do you work most closely with?"
value={formData.closeCollaborators || ''}
onChange={(value) => setFormData({ ...formData, closeCollaborators: value })}
onNext={() => handleNext()}
currentStep={1}
totalSteps={7}
sectionName="Team & Collaboration"
/>
);
case 14:
return (
<RatingScaleQuestion
question="How would you rate team communication overall?"
leftLabel="Poor"
rightLabel="Excellent"
value={formData.teamCommunication}
onChange={(value) => setFormData({ ...formData, teamCommunication: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={2}
totalSteps={7}
sectionName="Team & Collaboration"
scale={10}
/>
);
case 15:
return (
<TextAreaQuestion
question="Do you feel supported by your team? How?"
value={formData.teamSupport || ''}
onChange={(value) => setFormData({ ...formData, teamSupport: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={3}
totalSteps={7}
sectionName="Team & Collaboration"
/>
);
case 16:
return (
<SectionIntro
sectionNumber="4 of 6"
title="Tools & Resources"
description="Let's examine the tools and resources available to support your work."
onStart={() => handleNext()}
/>
);
case 17:
return (
<TextAreaQuestion
question="What tools and software do you currently use?"
value={formData.currentTools || ''}
onChange={(value) => setFormData({ ...formData, currentTools: value })}
onNext={() => handleNext()}
currentStep={1}
totalSteps={7}
sectionName="Tools & Resources"
/>
);
case 18:
return (
<RatingScaleQuestion
question="How effective are your current tools?"
leftLabel="Not effective"
rightLabel="Very effective"
value={formData.toolEffectiveness}
onChange={(value) => setFormData({ ...formData, toolEffectiveness: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={2}
totalSteps={7}
sectionName="Tools & Resources"
scale={10}
/>
);
case 19:
return (
<TextAreaQuestion
question="What tools or resources are you missing to do your job more effectively?"
value={formData.missingTools || ''}
onChange={(value) => setFormData({ ...formData, missingTools: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={3}
totalSteps={7}
sectionName="Tools & Resources"
/>
);
case 20:
return (
<SectionIntro
sectionNumber="5 of 6"
title="Skills & Development"
description="Let's explore your skills, growth opportunities, and career development."
onStart={() => handleNext()}
/>
);
case 21:
return (
<TextAreaQuestion
question="What are your key skills and strengths?"
value={formData.keySkills || ''}
onChange={(value) => setFormData({ ...formData, keySkills: value })}
onNext={() => handleNext()}
currentStep={1}
totalSteps={7}
sectionName="Skills & Development"
/>
);
case 22:
return (
<TextAreaQuestion
question="What skills would you like to develop or improve?"
value={formData.skillDevelopment || ''}
onChange={(value) => setFormData({ ...formData, skillDevelopment: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={2}
totalSteps={7}
sectionName="Skills & Development"
/>
);
case 23:
return (
<YesNoChoice
question="Are you aware of current training opportunities?"
value={formData.awareOfTraining}
onChange={(value) => setFormData({ ...formData, awareOfTraining: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={3}
totalSteps={7}
sectionName="Skills & Development"
/>
);
case 24:
return (
<TextAreaQuestion
question="What are your career goals within the company?"
value={formData.careerGoals || ''}
onChange={(value) => setFormData({ ...formData, careerGoals: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={4}
totalSteps={7}
sectionName="Skills & Development"
/>
);
case 25:
return (
<SectionIntro
sectionNumber="6 of 6"
title="Feedback & Improvement"
description="Finally, let's gather your thoughts on company improvements and overall satisfaction."
onStart={() => handleNext()}
/>
);
case 26:
return (
<TextAreaQuestion
question="What improvements would you suggest for the company?"
value={formData.companyImprovements || ''}
onChange={(value) => setFormData({ ...formData, companyImprovements: value })}
onNext={() => handleNext()}
currentStep={1}
totalSteps={7}
sectionName="Feedback & Improvement"
/>
);
case 27:
return (
<RatingScaleQuestion
question="How satisfied are you with your current job overall?"
leftLabel="Not satisfied"
rightLabel="Very satisfied"
value={formData.jobSatisfaction}
onChange={(value) => setFormData({ ...formData, jobSatisfaction: value })}
onBack={() => handleBack()}
onNext={() => handleNext()}
currentStep={2}
totalSteps={7}
sectionName="Feedback & Improvement"
scale={10}
/>
);
case 28:
return (
<TextAreaQuestion
question="Any additional feedback or suggestions for the company?"
value={formData.additionalFeedback || ''}
onChange={(value) => setFormData({ ...formData, additionalFeedback: value })}
onBack={() => handleBack()}
onNext={() => handleSubmit()}
currentStep={3}
totalSteps={7}
sectionName="Feedback & Improvement"
placeholder="Share any thoughts, suggestions, or feedback..."
/>
);
case 999: // Thank you page
return <ThankYouPage />;
default:
return <ThankYouPage />;
}
};
if (isSubmitting) {
return (
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
<div className="max-w-4xl mx-auto text-center">
<div className="w-16 h-16 bg-[--Brand-Orange] rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4 animate-pulse">
A
</div>
<h1 className="text-3xl font-bold text-[--Neutrals-NeutralSlate950] mb-4">Submitting Your Responses...</h1>
<p className="text-[--Neutrals-NeutralSlate500]">Please wait while we process your assessment and generate your report.</p>
</div>
</div>
);
}
return (
<div className="flex h-screen bg-[--Neutrals-NeutralSlate0]">
{renderStep()}
{error && (
<div className="fixed bottom-4 right-4 bg-red-500 text-white p-4 rounded-lg shadow-lg z-50">
{error}
</div>
)}
</div>
);
};
export default EmployeeQuestionnaire;

View File

@@ -1,381 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { useOrg } from '../contexts/OrgContext';
import { Card, Button } from '../components/UiKit';
import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers } from '../employeeQuestions';
import { Question } from '../components/ui/Question';
import { QuestionInput } from '../components/ui/QuestionInput';
import { FigmaQuestion } from '../components/figma/FigmaQuestion';
import { EnhancedFigmaQuestion } from '../components/figma/EnhancedFigmaQuestion';
import { LinearProgress } from '../components/ui/Progress';
import { Alert } from '../components/ui/Alert';
const EmployeeQuestionnaireSteps: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { user } = useAuth();
const { submitEmployeeAnswers, generateEmployeeReport, employees } = useOrg();
const [answers, setAnswers] = useState<EmployeeSubmissionAnswers>({});
const [currentStep, setCurrentStep] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
// Get employee info from multiple sources
const invitedEmployee = location.state?.invitedEmployee;
// Find current employee info - try multiple strategies
let currentEmployee = invitedEmployee || employees.find(emp => emp.email === user?.email);
// Additional matching strategies for edge cases
if (!currentEmployee && user?.email) {
// Try case-insensitive email matching
currentEmployee = employees.find(emp =>
emp.email?.toLowerCase() === user.email?.toLowerCase()
);
// Try matching by name if email doesn't work (for invite flow)
if (!currentEmployee && invitedEmployee) {
currentEmployee = employees.find(emp =>
emp.name === invitedEmployee.name || emp.id === invitedEmployee.id
);
}
}
// Demo mode fallbacks
if (!currentEmployee && user?.email === 'demo@auditly.local' && employees.length > 0) {
currentEmployee = employees[employees.length - 1];
}
if (!currentEmployee && employees.length === 1) {
currentEmployee = employees[0];
}
// Filter out followup questions that shouldn't be shown yet
const getVisibleQuestions = () => {
return EMPLOYEE_QUESTIONS.filter(question => {
if (!question.followupTo) return true;
const parentAnswer = answers[question.followupTo];
if (question.followupTo === 'has_kpis') {
return parentAnswer === 'Yes';
}
if (question.followupTo === 'unclear_responsibilities') {
return parentAnswer === 'Yes';
}
if (question.followupTo === 'role_shift_interest') {
return parentAnswer === 'Yes';
}
return false;
});
};
const visibleQuestions = getVisibleQuestions();
const currentQuestion = visibleQuestions[currentStep];
const handleAnswerChange = (value: string) => {
if (currentQuestion) {
setAnswers(prev => ({ ...prev, [currentQuestion.id]: value }));
}
};
const handleNext = () => {
if (currentStep < visibleQuestions.length - 1) {
setCurrentStep(prev => prev + 1);
} else {
handleSubmit();
}
};
const handleBack = () => {
if (currentStep > 0) {
setCurrentStep(prev => prev - 1);
}
};
const handleSubmit = async () => {
setIsSubmitting(true);
setError('');
try {
// Validate required questions
const requiredQuestions = visibleQuestions.filter(q => q.required);
const missingAnswers = requiredQuestions.filter(q => !answers[q.id]?.trim());
if (missingAnswers.length > 0) {
setError(`Please answer all required questions: ${missingAnswers.map(q => q.prompt).join(', ')}`);
setIsSubmitting(false);
return;
}
if (!currentEmployee) {
// Enhanced fallback logic
if (employees.length > 0) {
// Try to find employee by matching with the user's email more aggressively
let fallbackEmployee = employees.find(emp =>
emp.email?.toLowerCase().includes(user?.email?.toLowerCase().split('@')[0] || '')
);
// If still no match, use the most recent employee or one with matching domain
if (!fallbackEmployee) {
const userDomain = user?.email?.split('@')[1];
fallbackEmployee = employees.find(emp =>
emp.email?.split('@')[1] === userDomain
) || employees[employees.length - 1];
}
console.log('Using enhanced fallback employee:', fallbackEmployee);
const success = await submitEmployeeAnswers(fallbackEmployee.id, answers);
if (success) {
try {
const report = await generateEmployeeReport(fallbackEmployee);
if (report) {
console.log('Report generated successfully for fallback employee:', report);
}
} catch (reportError) {
console.error('Failed to generate report for fallback employee:', reportError);
}
navigate('/questionnaire-complete', {
replace: true,
state: {
employeeId: fallbackEmployee.id,
employeeName: fallbackEmployee.name,
message: 'Questionnaire submitted successfully! Your responses have been recorded.'
}
});
return;
}
}
setError(`We couldn't match your account (${user?.email}) with an employee record. Please contact your administrator to ensure your invite was set up correctly.`);
setIsSubmitting(false);
return;
}
const result = await submitEmployeeAnswers(currentEmployee.id, answers);
if (result) {
const message = result
? 'Questionnaire submitted successfully! Your AI-powered performance report has been generated.'
: 'Questionnaire submitted successfully! Your report will be available shortly.';
navigate('/questionnaire-complete', {
state: {
employeeId: currentEmployee.id,
employeeName: currentEmployee.name,
reportGenerated: result,
message: message
}
});
} else {
setError(result.message || 'Failed to submit questionnaire');
}
} catch (error) {
console.error('Submission error:', error);
setError('Failed to submit questionnaire. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const getProgressPercentage = () => {
return Math.round(((currentStep + 1) / visibleQuestions.length) * 100);
};
const isCurrentQuestionAnswered = () => {
if (!currentQuestion) return false;
const answer = answers[currentQuestion.id];
return answer && answer.trim().length > 0;
};
if (!currentQuestion) {
return (
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
<div className="max-w-4xl mx-auto text-center">
<h1 className="text-2xl font-bold text-[--text-primary] mb-4">
No questions available
</h1>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
A
</div>
<h1 className="text-3xl font-bold text-[--text-primary] mb-2">
Welcome to Auditly!
</h1>
<p className="text-[--text-secondary] mb-4">
Please complete this questionnaire to help us understand your role and create personalized insights.
</p>
{currentEmployee ? (
<div className="inline-flex items-center px-4 py-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
<span className="text-sm text-blue-800 dark:text-blue-200">
👋 Hello {currentEmployee.name}! {currentEmployee.role && `(${currentEmployee.role})`}
</span>
</div>
) : (
<div className="space-y-2">
<div className="inline-flex items-center px-4 py-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
<span className="text-sm text-yellow-800 dark:text-yellow-200">
Employee info not found. User: {user?.email}, Employees: {employees.length}
</span>
</div>
<div className="text-xs text-[--text-secondary] max-w-md mx-auto">
<p>Don't worry - your account was created successfully! This is likely a temporary sync issue.</p>
<p className="mt-1">You can still complete the questionnaire, and we'll match it to your profile automatically.</p>
</div>
</div>
)}
</div>
{/* Progress Bar */}
<div className="mb-8">
<div className="flex justify-between items-center mb-2">
<span className="text-sm text-[--text-secondary]">
Question {currentStep + 1} of {visibleQuestions.length}
</span>
<span className="text-sm text-[--text-secondary]">{getProgressPercentage()}%</span>
</div>
<LinearProgress value={getProgressPercentage()} />
</div>
{/* Enhanced Question Card */}
<div className="flex justify-center mb-8">
<div className="w-full max-w-2xl">
<EnhancedFigmaQuestion
questionNumber={`Q${currentStep + 1}`}
question={currentQuestion.prompt}
answer={answers[currentQuestion.id] || ''}
onAnswerChange={handleAnswerChange}
onBack={currentStep > 0 ? handleBack : undefined}
onNext={
currentStep < visibleQuestions.length - 1
? handleNext
: (isCurrentQuestionAnswered() || !currentQuestion.required)
? handleSubmit
: undefined
}
nextLabel={
currentStep < visibleQuestions.length - 1
? 'Next'
: isSubmitting
? 'Submitting...'
: 'Submit & Generate Report'
}
showNavigation={true}
/>
</div>
</div>
{/* Original Figma Question Card for Comparison */}
<div className="flex justify-center mb-8">
<div className="w-full max-w-2xl">
<FigmaQuestion
questionNumber={`Q${currentStep + 1}`}
title={currentQuestion.prompt}
description={currentQuestion.required ? 'Required' : 'Optional'}
answer={answers[currentQuestion.id] || ''}
onAnswerChange={handleAnswerChange}
onBack={currentStep > 0 ? handleBack : undefined}
onNext={
currentStep < visibleQuestions.length - 1
? handleNext
: (isCurrentQuestionAnswered() || !currentQuestion.required)
? handleSubmit
: undefined
}
nextLabel={
currentStep < visibleQuestions.length - 1
? 'Next'
: isSubmitting
? 'Submitting...'
: 'Submit & Generate Report'
}
showNavigation={true}
/>
</div>
</div>
{/* Alternative Input for Different Question Types */}
<div className="flex justify-center mb-8">
<div className="w-full max-w-2xl">
<Card className="p-6">
<Question
label={`${currentStep + 1}. ${currentQuestion.prompt}`}
required={currentQuestion.required}
description={`Category: ${currentQuestion.category}`}
>
<QuestionInput
question={currentQuestion}
value={answers[currentQuestion.id] || ''}
onChange={handleAnswerChange}
/>
</Question>
</Card>
</div>
</div>
{/* Error Display */}
{error && (
<div className="mb-6 flex justify-center">
<div className="w-full max-w-2xl">
<Alert variant="error" title="Error">
{error}
</Alert>
</div>
</div>
)}
{/* Navigation Buttons */}
<div className="flex justify-center gap-4">
{currentStep > 0 && (
<Button
variant="secondary"
onClick={handleBack}
disabled={isSubmitting}
>
Back
</Button>
)}
{currentStep < visibleQuestions.length - 1 ? (
<Button
onClick={handleNext}
disabled={currentQuestion.required && !isCurrentQuestionAnswered()}
>
Next
</Button>
) : (
<Button
onClick={handleSubmit}
disabled={
isSubmitting ||
(currentQuestion.required && !isCurrentQuestionAnswered())
}
>
{isSubmitting ? 'Submitting & Generating Report...' : 'Submit & Generate AI Report'}
</Button>
)}
</div>
{/* Help Text */}
<div className="text-center mt-6">
<p className="text-sm text-[--text-secondary]">
{currentQuestion.required && !isCurrentQuestionAnswered()
? 'Please answer this required question to continue.'
: 'You can skip optional questions or come back to them later.'
}
</p>
</div>
</div>
</div>
);
};
export default EmployeeQuestionnaireSteps;

View File

@@ -1,394 +0,0 @@
// DEPRECATED: This component has been split into separate Help and Settings pages
// Use /src/pages/HelpNew.tsx and /src/pages/SettingsNew.tsx instead
// This file can be safely removed in future cleanup
import React, { useState } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import { useAuth } from '../contexts/AuthContext';
import { useOrg } from '../contexts/OrgContext';
import { Card, Button } from '../components/UiKit';
import { Theme } from '../types';
const HelpAndSettings: React.FC = () => {
const { theme, setTheme } = useTheme();
const { user, signOutUser } = useAuth();
const { org, upsertOrg, issueInviteViaApi } = useOrg();
const [activeTab, setActiveTab] = useState<'settings' | 'help'>('settings');
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
const [isInviting, setIsInviting] = useState(false);
const [inviteResult, setInviteResult] = useState<string | null>(null);
const handleLogout = async () => {
try {
await signOutUser();
} catch (error) {
console.error('Logout error:', error);
}
};
const handleRestartOnboarding = async () => {
try {
await upsertOrg({ onboardingCompleted: false });
// The RequireOnboarding component will redirect automatically
} catch (error) {
console.error('Failed to restart onboarding:', error);
}
};
const handleInviteEmployee = async () => {
if (!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting) return;
setIsInviting(true);
setInviteResult(null);
try {
const result = await issueInviteViaApi({
name: inviteForm.name.trim(),
email: inviteForm.email.trim(),
role: inviteForm.role.trim() || undefined,
department: inviteForm.department.trim() || undefined
});
setInviteResult(JSON.stringify({
success: true,
inviteLink: result.inviteLink,
emailLink: result.emailLink,
employeeName: result.employee.name
}));
setInviteForm({ name: '', email: '', role: '', department: '' });
} catch (error) {
console.error('Failed to send invitation:', error);
setInviteResult('Failed to send invitation. Please try again.');
} finally {
setIsInviting(false);
}
};
const renderSettings = () => (
<div className="space-y-6">
<Card>
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Appearance</h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">
Theme
</label>
<div className="flex space-x-2">
<Button
variant={theme === Theme.Light ? 'primary' : 'secondary'}
size="sm"
onClick={() => setTheme(Theme.Light)}
>
Light
</Button>
<Button
variant={theme === Theme.Dark ? 'primary' : 'secondary'}
size="sm"
onClick={() => setTheme(Theme.Dark)}
>
Dark
</Button>
<Button
variant={theme === Theme.System ? 'primary' : 'secondary'}
size="sm"
onClick={() => setTheme(Theme.System)}
>
System
</Button>
</div>
</div>
</div>
</Card>
<Card>
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Organization</h3>
<div className="space-y-3">
<div>
<span className="text-sm text-[--text-secondary]">Company:</span>
<div className="font-medium text-[--text-primary]">{org?.name}</div>
</div>
<div>
<span className="text-sm text-[--text-secondary]">Onboarding:</span>
<div className="font-medium text-[--text-primary]">
{org?.onboardingCompleted ? 'Completed' : 'Incomplete'}
</div>
</div>
<div className="pt-4">
<Button variant="secondary" onClick={handleRestartOnboarding}>
Restart Onboarding
</Button>
<p className="text-xs text-[--text-secondary] mt-2">
This will reset your company profile and require you to complete the setup process again.
</p>
</div>
</div>
</Card>
<Card>
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-1">
Name *
</label>
<input
type="text"
value={inviteForm.name}
onChange={(e) => setInviteForm(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="John Doe"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-1">
Email *
</label>
<input
type="email"
value={inviteForm.email}
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="john.doe@company.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-1">
Role
</label>
<input
type="text"
value={inviteForm.role}
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Senior Developer"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-1">
Department
</label>
<input
type="text"
value={inviteForm.department}
onChange={(e) => setInviteForm(prev => ({ ...prev, department: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Engineering"
/>
</div>
</div>
<Button
onClick={handleInviteEmployee}
disabled={!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting}
className="w-full"
>
{isInviting ? 'Sending Invitation...' : 'Send Invitation'}
</Button>
{inviteResult && (
<div>
{inviteResult.includes('Failed') ? (
<div className="p-3 rounded-md text-sm bg-red-50 text-red-800 border border-red-200">
{inviteResult}
</div>
) : (
(() => {
try {
const result = JSON.parse(inviteResult);
return (
<div className="p-4 rounded-md bg-green-50 border border-green-200">
<h4 className="text-sm font-semibold text-green-800 mb-3">
Invitation sent to {result.employeeName}!
</h4>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-green-700 mb-1">
Direct Link (share this with the employee):
</label>
<div className="flex gap-2">
<input
type="text"
value={result.inviteLink}
readOnly
className="flex-1 px-2 py-1 text-xs bg-white border border-green-300 rounded font-mono"
/>
<Button
size="sm"
variant="secondary"
onClick={() => navigator.clipboard.writeText(result.inviteLink)}
>
Copy
</Button>
</div>
</div>
<div>
<a
href={result.emailLink}
className="inline-flex items-center px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
>
📧 Open Email Draft
</a>
</div>
</div>
</div>
);
} catch {
return (
<div className="p-3 rounded-md text-sm bg-green-50 text-green-800 border border-green-200">
{inviteResult}
</div>
);
}
})()
)}
</div>
)}
<p className="text-xs text-[--text-secondary]">
The invited employee will receive an email with instructions to join your organization.
</p>
</div>
</Card>
<Card>
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Account</h3>
<div className="space-y-3">
<div>
<span className="text-sm text-[--text-secondary]">Email:</span>
<div className="font-medium text-[--text-primary]">{user?.email}</div>
</div>
<div>
<span className="text-sm text-[--text-secondary]">User ID:</span>
<div className="font-medium text-[--text-primary] font-mono text-xs">{user?.uid}</div>
</div>
<div className="pt-4">
<Button variant="secondary" onClick={handleLogout}>
Sign Out
</Button>
</div>
</div>
</Card>
<Card>
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Data & Privacy</h3>
<div className="space-y-3">
<Button variant="secondary" className="w-full justify-start">
Export My Data
</Button>
<Button variant="secondary" className="w-full justify-start">
Privacy Settings
</Button>
<Button variant="secondary" className="w-full justify-start text-red-600">
Delete Account
</Button>
</div>
</Card>
</div>
);
const renderHelp = () => (
<div className="space-y-6">
<Card>
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Getting Started</h3>
<div className="space-y-4">
<div>
<h4 className="font-medium text-[--text-primary] mb-2">1. Set up your organization</h4>
<p className="text-[--text-secondary] text-sm">
Complete the onboarding process to configure your company information and preferences.
</p>
</div>
<div>
<h4 className="font-medium text-[--text-primary] mb-2">2. Add employees</h4>
<p className="text-[--text-secondary] text-sm">
Invite team members and add their basic information to start generating reports.
</p>
</div>
<div>
<h4 className="font-medium text-[--text-primary] mb-2">3. Generate reports</h4>
<p className="text-[--text-secondary] text-sm">
Use AI-powered reports to gain insights into employee performance and organizational health.
</p>
</div>
</div>
</Card>
<Card>
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Frequently Asked Questions</h3>
<div className="space-y-4">
<div>
<h4 className="font-medium text-[--text-primary] mb-2">How do I add new employees?</h4>
<p className="text-[--text-secondary] text-sm">
Go to the Reports page and use the "Add Employee" button to invite new team members.
</p>
</div>
<div>
<h4 className="font-medium text-[--text-primary] mb-2">How are reports generated?</h4>
<p className="text-[--text-secondary] text-sm">
Reports use AI to analyze employee data and provide insights on performance, strengths, and development opportunities.
</p>
</div>
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Is my data secure?</h4>
<p className="text-[--text-secondary] text-sm">
Yes, all data is encrypted and stored securely. We follow industry best practices for data protection.
</p>
</div>
</div>
</Card>
<Card>
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Contact Support</h3>
<div className="space-y-3">
<Button variant="secondary" className="w-full justify-start">
📧 Email Support
</Button>
<Button variant="secondary" className="w-full justify-start">
💬 Live Chat
</Button>
<Button variant="secondary" className="w-full justify-start">
📚 Documentation
</Button>
</div>
</Card>
</div>
);
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="mb-6">
<h1 className="text-3xl font-bold text-[--text-primary]">Help & Settings</h1>
<p className="text-[--text-secondary] mt-1">
Manage your account and get help
</p>
</div>
<div className="mb-6">
<div className="flex space-x-4 border-b border-[--border-color]">
<button
onClick={() => setActiveTab('settings')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'settings'
? 'border-blue-500 text-blue-500'
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
}`}
>
Settings
</button>
<button
onClick={() => setActiveTab('help')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'help'
? 'border-blue-500 text-blue-500'
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
}`}
>
Help
</button>
</div>
</div>
{activeTab === 'settings' ? renderSettings() : renderHelp()}
</div>
);
};
export default HelpAndSettings;

View File

@@ -1,69 +0,0 @@
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { CheckCircle, FileText, Sparkles } from 'lucide-react';
const QuestionnaireComplete: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const {
employeeName = 'Employee',
reportGenerated = false,
message = 'Thank you for completing the questionnaire!'
} = location.state || {};
return (
<div className="min-h-screen bg-[--background-primary] flex items-center justify-center p-4">
<div className="max-w-md w-full bg-[--background-secondary] rounded-lg shadow-lg p-8 text-center">
<div className="flex justify-center mb-6">
{reportGenerated ? (
<div className="relative">
<CheckCircle className="w-16 h-16 text-green-500" />
<Sparkles className="w-6 h-6 text-yellow-400 absolute -top-1 -right-1" />
</div>
) : (
<CheckCircle className="w-16 h-16 text-green-500" />
)}
</div>
<h1 className="text-2xl font-bold text-[--text-primary] mb-4">
Questionnaire Complete!
</h1>
<p className="text-[--text-secondary] mb-6">
{message}
</p>
{reportGenerated && (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
<div className="flex items-center justify-center mb-2">
<FileText className="w-5 h-5 text-blue-600 mr-2" />
<span className="text-sm font-medium text-blue-600">AI Report Generated</span>
</div>
<p className="text-sm text-blue-600 dark:text-blue-300">
Your personalized performance report has been created using AI analysis of your responses.
</p>
</div>
)}
<div className="space-y-3">
<button
onClick={() => navigate('/reports')}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
{reportGenerated ? 'View Your Report' : 'Go to Dashboard'}
</button>
<button
onClick={() => navigate('/')}
className="w-full bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
Return to Home
</button>
</div>
</div>
</div>
);
};
export default QuestionnaireComplete;

297
src/pages/Settings.tsx Normal file
View File

@@ -0,0 +1,297 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '../contexts/ThemeContext';
import { useAuth } from '../contexts/AuthContext';
import Sidebar from '../components/figma/Sidebar';
import { Button } from '../components/UiKit';
import { Theme } from '../types';
interface UserProfile {
fullName: string;
email: string;
profilePicture?: string;
}
type ThemeMode = 'system' | 'light' | 'dark';
const SettingsNew: React.FC = () => {
const { theme, setTheme } = useTheme();
const { user } = useAuth();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<'general' | 'billing'>('general');
const [userProfile, setUserProfile] = useState<UserProfile>({
fullName: 'John Doe',
email: 'Johndoe1234@gmail.com'
});
const [selectedTheme, setSelectedTheme] = useState<ThemeMode>('light');
const handleProfileUpdate = (field: keyof UserProfile, value: string) => {
setUserProfile(prev => ({
...prev,
[field]: value
}));
};
const handlePhotoUpload = () => {
// In a real app, this would open a file picker
alert('Photo upload functionality would be implemented here');
};
const handleSaveChanges = () => {
// In a real app, this would save to backend
alert('Settings saved successfully!');
};
const handleReset = () => {
setUserProfile({
fullName: 'John Doe',
email: 'Johndoe1234@gmail.com'
});
setSelectedTheme('light');
};
if (!user) {
navigate('/login');
return null;
}
return (
<div className="flex-1 self-stretch shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start h-full">
<Sidebar companyName="Zitlac Media" />
<div className="flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-start items-start">
{/* Tab Navigation */}
<div className="self-stretch px-6 pt-6 border-b border-[--Neutrals-NeutralSlate200] flex flex-col justify-start items-end">
<div className="self-stretch inline-flex justify-start items-start gap-6">
<div
onClick={() => setActiveTab('general')}
className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'general' ? '' : 'opacity-60'
}`}
>
<div className={`self-stretch text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'general'
? 'text-Text-Gray-800 font-semibold'
: 'text-Text-Gray-500 font-normal'
}`}>
General Settings
</div>
{activeTab === 'general' && (
<div className="self-stretch h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
)}
</div>
<div
onClick={() => setActiveTab('billing')}
className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'billing' ? '' : 'opacity-60'
}`}
>
<div className={`text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'billing'
? 'text-Text-Gray-800 font-semibold'
: 'text-Text-Gray-500 font-normal'
}`}>
Plan & Billings
</div>
{activeTab === 'billing' && (
<div className="w-24 h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
)}
</div>
</div>
<div className="w-24 h-0.5 opacity-0 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
</div>
{/* General Settings Content */}
{activeTab === 'general' && (
<>
{/* Profile Information Section */}
<div className="w-full h-72 p-6 flex flex-col justify-start items-start gap-6">
<div className="flex flex-col justify-start items-start gap-1">
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-lg font-semibold font-['Inter'] leading-7">Profile Information</div>
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate500] text-sm font-normal font-['Inter'] leading-tight">Update your personal details, and keep your profile up to date.</div>
</div>
<div className="self-stretch flex flex-col justify-start items-start gap-6">
{/* Profile Picture Section */}
<div className="w-[664px] px-3 py-2.5 bg-[--Neutrals-NeutralSlate0] rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex justify-between items-center">
<div className="flex-1 flex justify-start items-center gap-3">
<div className="w-14 h-14 relative bg-red-200 rounded-[999px]">
<div>
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_1042_3786)">
<ellipse cx="28" cy="54.6008" rx="22.4" ry="16.8" fill="white" fillOpacity="0.72" />
<circle opacity="0.9" cx="28" cy="22.3992" r="11.2" fill="white" />
</g>
<defs>
<clipPath id="clip0_1042_3786">
<rect width="56" height="56" rx="28" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</div>
<div className="flex-1 inline-flex flex-col justify-start items-start gap-1">
<div className="self-stretch justify-center text-[--Neutrals-NeutralSlate800] text-base font-semibold font-['Inter'] leading-normal">Profile Picture</div>
<div className="self-stretch justify-center text-[--Neutrals-NeutralSlate500] text-xs font-normal font-['Inter'] leading-none">PNG, JPEG, GIF Under 10MB</div>
</div>
</div>
<div
onClick={handlePhotoUpload}
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
>
<div>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66665 10.8282C1.86266 10.29 1.33331 9.37347 1.33331 8.33333C1.33331 6.77095 2.52765 5.48753 4.05314 5.34625C4.36519 3.44809 6.01348 2 7.99998 2C9.98648 2 11.6348 3.44809 11.9468 5.34625C13.4723 5.48753 14.6666 6.77095 14.6666 8.33333C14.6666 9.37347 14.1373 10.29 13.3333 10.8282M5.33331 10.6667L7.99998 8M7.99998 8L10.6666 10.6667M7.99998 8V14" stroke="var(--Neutrals-NeutralSlate950)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Upload Photo</div>
</div>
</div>
</div>
{/* Name and Email Fields */}
<div className="w-[664px] inline-flex justify-start items-center gap-4">
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Full Name</div>
</div>
<div className="self-stretch flex flex-col justify-start items-start gap-1">
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
<div>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12.5C7.35828 12.5 5.00901 13.7755 3.51334 15.755C3.19143 16.181 3.03047 16.394 3.03574 16.6819C3.03981 16.9043 3.17948 17.1849 3.35448 17.3222C3.581 17.5 3.8949 17.5 4.5227 17.5H15.4773C16.1051 17.5 16.419 17.5 16.6455 17.3222C16.8205 17.1849 16.9602 16.9043 16.9643 16.6819C16.9695 16.394 16.8086 16.181 16.4867 15.755C14.991 13.7755 12.6417 12.5 10 12.5Z" stroke="var(--Neutrals-NeutralSlate600)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10 10C12.0711 10 13.75 8.32107 13.75 6.25C13.75 4.17893 12.0711 2.5 10 2.5C7.92894 2.5 6.25001 4.17893 6.25001 6.25C6.25001 8.32107 7.92894 10 10 10Z" stroke="var(--Neutrals-NeutralSlate600)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<input
type="text"
value={userProfile.fullName}
onChange={(e) => handleProfileUpdate('fullName', e.target.value)}
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
/>
</div>
</div>
</div>
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Email Address</div>
</div>
<div className="self-stretch flex flex-col justify-start items-start gap-1">
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
<div>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.66669 5.83203L8.47079 10.5949C9.02176 10.9806 9.29725 11.1734 9.59691 11.2481C9.8616 11.3141 10.1384 11.3141 10.4031 11.2481C10.7028 11.1734 10.9783 10.9806 11.5293 10.5949L18.3334 5.83203M5.66669 16.6654H14.3334C15.7335 16.6654 16.4336 16.6654 16.9683 16.3929C17.4387 16.1532 17.8212 15.7707 18.0609 15.3003C18.3334 14.7656 18.3334 14.0655 18.3334 12.6654V7.33203C18.3334 5.9319 18.3334 5.23183 18.0609 4.69705C17.8212 4.22665 17.4387 3.8442 16.9683 3.60451C16.4336 3.33203 15.7335 3.33203 14.3334 3.33203H5.66669C4.26656 3.33203 3.56649 3.33203 3.03171 3.60451C2.56131 3.8442 2.17885 4.22665 1.93917 4.69705C1.66669 5.23183 1.66669 5.9319 1.66669 7.33203V12.6654C1.66669 14.0655 1.66669 14.7656 1.93917 15.3003C2.17885 15.7707 2.56131 16.1532 3.03171 16.3929C3.56649 16.6654 4.26656 16.6654 5.66669 16.6654Z" stroke="var(--Neutrals-NeutralSlate600)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<input
type="email"
value={userProfile.email}
onChange={(e) => handleProfileUpdate('email', e.target.value)}
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
/>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Divider */}
<div>
<svg width="100%" height="2" viewBox="0 0 2000 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1H2000" stroke="var(--Neutrals-NeutralSlate200)" />
</svg>
</div>
{/* Theme Customization Section */}
<div className="w-full self-stretch p-6 flex flex-col justify-start items-start gap-6">
<div className="w-[584px] flex flex-col justify-start items-start gap-1">
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-lg font-semibold font-['Inter'] leading-7">Theme Customization</div>
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate500] text-sm font-normal font-['Inter'] leading-tight">Personalize your interface with light or dark mode and enhance your visual experience.</div>
</div>
<div className="inline-flex justify-start items-start gap-3 flex-wrap content-start">
{/* System Preference */}
<div
onClick={() => setSelectedTheme('system')}
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'system' ? 'opacity-100' : 'opacity-70'
}`}
>
<button className="inline-flex justify-start items-center" onClick={() => setTheme(Theme.System)}>
<img className="w-24 h-28 rounded-tl-lg rounded-bl-lg" src="/image/39a0d5e73dec8bece795a718c5800f02df8f8631.png"></img>
<img className="w-24 h-28 rounded-tr-lg rounded-br-lg" src="/image/bc07fdc9eec8a78357aaf70e9deae41d4b7a7d2d.png"></img>
</button>
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">System preference</div>
</div>
{/* Light Mode */}
<div
onClick={() => setSelectedTheme('light')}
className={`w-48 max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'light' ? 'opacity-100' : 'opacity-70'
}`}
>
<div className="self-stretch h-28 relative bg-[--Neutrals-NeutralSlate0] rounded-lg overflow-hidden">
<div className={`w-48 h-28 left-0 top-0 absolute bg-[--Neutrals-NeutralSlate0] rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'light' ? 'outline-Brand-Orange' : 'outline-[--Neutrals-NeutralSlate200]'
}`}>
<button className="w-48 h-28 left-0 top-0 absolute rounded-lg" style={{ backgroundImage: 'url("/image/39a0d5e73dec8bece795a718c5800f02df8f8631.png")', backgroundSize: "cover" }} onClick={() => setTheme(Theme.Light)}></button>
</div>
</div>
<div className="self-stretch h-5 justify-start text-[--Neutrals-NeutralSlate800] text-sm font-normal font-['Inter'] leading-tight">Light Mode</div>
</div>
{/* Dark Mode */}
<div
onClick={() => setSelectedTheme('dark')}
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'dark' ? 'opacity-100' : 'opacity-70'
}`}
>
<div className="w-48 h-28 relative bg-[--Neutrals-NeutralSlate950] rounded-lg overflow-hidden">
<div className={`w-48 h-28 left-0 top-0 absolute bg-[--Neutrals-NeutralSlate0] rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'dark' ? 'outline-Brand-Orange' : 'outline-[--Neutrals-NeutralSlate200]'
}`}>
<button className="w-48 h-28 left-0 top-0 absolute rounded-lg" style={{ backgroundImage: 'url("/image/bc07fdc9eec8a78357aaf70e9deae41d4b7a7d2d.png")', backgroundSize: "cover" }} onClick={() => setTheme(Theme.Dark)}></button>
</div>
</div>
<div className="self-stretch h-5 justify-start text-[--Neutrals-NeutralSlate800] text-sm font-normal font-['Inter'] leading-tight">Dark Mode</div>
</div>
</div>
</div>
{/* Another Divider */}
<div>
<svg width="2000" height="2" viewBox="0 0 2000 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1H2000" stroke="var(--Neutrals-NeutralSlate200)" />
</svg>
</div>
{/* Action Buttons */}
<div className="w-[1175px] p-6 inline-flex justify-start items-center gap-2">
<div
onClick={handleReset}
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Reset</div>
</div>
</div>
<div
onClick={handleSaveChanges}
className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Brand-Orange]/90"
>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Save Changes</div>
</div>
</div>
</div>
</>
)}
{/* Billing Content */}
{activeTab === 'billing' && (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-semibold text-[--Neutrals-NeutralSlate800] mb-4">Plan & Billing</h2>
<p className="text-[--Neutrals-NeutralSlate500]">Billing management features would be implemented here.</p>
</div>
</div>
)}
</div>
</div>
);
};
export default SettingsNew;

View File

@@ -1,295 +0,0 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import Sidebar from '../components/figma/Sidebar';
interface UserProfile {
fullName: string;
email: string;
profilePicture?: string;
}
type ThemeMode = 'system' | 'light' | 'dark';
const SettingsNew: React.FC = () => {
const { user } = useAuth();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<'general' | 'billing'>('general');
const [userProfile, setUserProfile] = useState<UserProfile>({
fullName: 'John Doe',
email: 'Johndoe1234@gmail.com'
});
const [selectedTheme, setSelectedTheme] = useState<ThemeMode>('light');
const handleProfileUpdate = (field: keyof UserProfile, value: string) => {
setUserProfile(prev => ({
...prev,
[field]: value
}));
};
const handlePhotoUpload = () => {
// In a real app, this would open a file picker
alert('Photo upload functionality would be implemented here');
};
const handleSaveChanges = () => {
// In a real app, this would save to backend
alert('Settings saved successfully!');
};
const handleReset = () => {
setUserProfile({
fullName: 'John Doe',
email: 'Johndoe1234@gmail.com'
});
setSelectedTheme('light');
};
if (!user) {
navigate('/login');
return null;
}
return (
<div className="w-[1440px] h-[840px] p-4 bg-[--Neutrals-NeutralSlate200] inline-flex justify-start items-start overflow-hidden">
<div className="flex-1 self-stretch rounded-3xl shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start overflow-hidden">
<Sidebar companyName="Zitlac Media" />
<div className="flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-start items-start">
{/* Tab Navigation */}
<div className="self-stretch px-6 pt-6 border-b border-Outline-Outline-Gray-200 flex flex-col justify-start items-end">
<div className="self-stretch inline-flex justify-start items-start gap-6">
<div
onClick={() => setActiveTab('general')}
className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'general' ? '' : 'opacity-60'
}`}
>
<div className={`self-stretch text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'general'
? 'text-Text-Gray-800 font-semibold'
: 'text-Text-Gray-500 font-normal'
}`}>
General Settings
</div>
{activeTab === 'general' && (
<div className="self-stretch h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
)}
</div>
<div
onClick={() => setActiveTab('billing')}
className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'billing' ? '' : 'opacity-60'
}`}
>
<div className={`text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'billing'
? 'text-Text-Gray-800 font-semibold'
: 'text-Text-Gray-500 font-normal'
}`}>
Plan & Billings
</div>
{activeTab === 'billing' && (
<div className="w-24 h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
)}
</div>
</div>
<div className="w-24 h-0.5 opacity-0 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
</div>
{/* General Settings Content */}
{activeTab === 'general' && (
<>
{/* Profile Information Section */}
<div className="w-[1136px] h-72 p-6 flex flex-col justify-start items-start gap-6">
<div className="w-[584px] flex flex-col justify-start items-start gap-1">
<div className="self-stretch justify-start text-Text-Gray-800 text-lg font-semibold font-['Inter'] leading-7">Profile Information</div>
<div className="self-stretch justify-start text-Text-Gray-500 text-sm font-normal font-['Inter'] leading-tight">Update your personal details, and keep your profile up to date.</div>
</div>
<div className="self-stretch flex flex-col justify-start items-start gap-6">
{/* Profile Picture Section */}
<div className="w-[664px] px-3 py-2.5 bg-Text-White-00 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 inline-flex justify-between items-center">
<div className="flex-1 flex justify-start items-center gap-3">
<div className="w-14 h-14 relative bg-red-200 rounded-[999px]">
<div>
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_1042_3786)">
<ellipse cx="28" cy="54.6008" rx="22.4" ry="16.8" fill="white" fillOpacity="0.72" />
<circle opacity="0.9" cx="28" cy="22.3992" r="11.2" fill="white" />
</g>
<defs>
<clipPath id="clip0_1042_3786">
<rect width="56" height="56" rx="28" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</div>
<div className="flex-1 inline-flex flex-col justify-start items-start gap-1">
<div className="self-stretch justify-center text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Profile Picture</div>
<div className="self-stretch justify-center text-Text-Gray-500 text-xs font-normal font-['Inter'] leading-none">PNG, JPEG, GIF Under 10MB</div>
</div>
</div>
<div
onClick={handlePhotoUpload}
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
>
<div>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66665 10.8282C1.86266 10.29 1.33331 9.37347 1.33331 8.33333C1.33331 6.77095 2.52765 5.48753 4.05314 5.34625C4.36519 3.44809 6.01348 2 7.99998 2C9.98648 2 11.6348 3.44809 11.9468 5.34625C13.4723 5.48753 14.6666 6.77095 14.6666 8.33333C14.6666 9.37347 14.1373 10.29 13.3333 10.8282M5.33331 10.6667L7.99998 8M7.99998 8L10.6666 10.6667M7.99998 8V14" stroke="var(--Text-Dark-950, #0A0D12)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Upload Photo</div>
</div>
</div>
</div>
{/* Name and Email Fields */}
<div className="w-[664px] inline-flex justify-start items-center gap-4">
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Full Name</div>
</div>
<div className="self-stretch flex flex-col justify-start items-start gap-1">
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
<div>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12.5C7.35828 12.5 5.00901 13.7755 3.51334 15.755C3.19143 16.181 3.03047 16.394 3.03574 16.6819C3.03981 16.9043 3.17948 17.1849 3.35448 17.3222C3.581 17.5 3.8949 17.5 4.5227 17.5H15.4773C16.1051 17.5 16.419 17.5 16.6455 17.3222C16.8205 17.1849 16.9602 16.9043 16.9643 16.6819C16.9695 16.394 16.8086 16.181 16.4867 15.755C14.991 13.7755 12.6417 12.5 10 12.5Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10 10C12.0711 10 13.75 8.32107 13.75 6.25C13.75 4.17893 12.0711 2.5 10 2.5C7.92894 2.5 6.25001 4.17893 6.25001 6.25C6.25001 8.32107 7.92894 10 10 10Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<input
type="text"
value={userProfile.fullName}
onChange={(e) => handleProfileUpdate('fullName', e.target.value)}
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
/>
</div>
</div>
</div>
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Email Address</div>
</div>
<div className="self-stretch flex flex-col justify-start items-start gap-1">
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
<div>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.66669 5.83203L8.47079 10.5949C9.02176 10.9806 9.29725 11.1734 9.59691 11.2481C9.8616 11.3141 10.1384 11.3141 10.4031 11.2481C10.7028 11.1734 10.9783 10.9806 11.5293 10.5949L18.3334 5.83203M5.66669 16.6654H14.3334C15.7335 16.6654 16.4336 16.6654 16.9683 16.3929C17.4387 16.1532 17.8212 15.7707 18.0609 15.3003C18.3334 14.7656 18.3334 14.0655 18.3334 12.6654V7.33203C18.3334 5.9319 18.3334 5.23183 18.0609 4.69705C17.8212 4.22665 17.4387 3.8442 16.9683 3.60451C16.4336 3.33203 15.7335 3.33203 14.3334 3.33203H5.66669C4.26656 3.33203 3.56649 3.33203 3.03171 3.60451C2.56131 3.8442 2.17885 4.22665 1.93917 4.69705C1.66669 5.23183 1.66669 5.9319 1.66669 7.33203V12.6654C1.66669 14.0655 1.66669 14.7656 1.93917 15.3003C2.17885 15.7707 2.56131 16.1532 3.03171 16.3929C3.56649 16.6654 4.26656 16.6654 5.66669 16.6654Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<input
type="email"
value={userProfile.email}
onChange={(e) => handleProfileUpdate('email', e.target.value)}
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
/>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Divider */}
<div>
<svg width="1136" height="2" viewBox="0 0 1136 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1H1136" stroke="var(--Text-Gray-200, #E9EAEB)" />
</svg>
</div>
{/* Theme Customization Section */}
<div className="w-[1170px] p-6 flex flex-col justify-start items-start gap-6">
<div className="w-[584px] flex flex-col justify-start items-start gap-1">
<div className="self-stretch justify-start text-Text-Gray-800 text-lg font-semibold font-['Inter'] leading-7">Theme Customization</div>
<div className="self-stretch justify-start text-Text-Gray-500 text-sm font-normal font-['Inter'] leading-tight">Personalize your interface with light or dark mode and enhance your visual experience.</div>
</div>
<div className="inline-flex justify-start items-start gap-3 flex-wrap content-start">
{/* System Preference */}
<div
onClick={() => setSelectedTheme('system')}
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'system' ? 'opacity-100' : 'opacity-70'
}`}
>
<div className="inline-flex justify-start items-center">
<img className="w-24 h-28 rounded-tl-lg rounded-bl-lg" src="https://via.placeholder.com/94x107/f8f9fa/6c757d?text=Light" />
<img className="w-24 h-28 rounded-tr-lg rounded-br-lg" src="https://via.placeholder.com/96x107/212529/ffffff?text=Dark" />
</div>
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">System preference</div>
</div>
{/* Light Mode */}
<div
onClick={() => setSelectedTheme('light')}
className={`w-48 max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'light' ? 'opacity-100' : 'opacity-70'
}`}
>
<div className="self-stretch h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'light' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
}`}>
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/f8f9fa/6c757d?text=Light+Mode" />
</div>
</div>
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">Light Mode</div>
</div>
{/* Dark Mode */}
<div
onClick={() => setSelectedTheme('dark')}
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'dark' ? 'opacity-100' : 'opacity-70'
}`}
>
<div className="w-48 h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'dark' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
}`}>
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/212529/ffffff?text=Dark+Mode" />
</div>
</div>
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">Dark Mode</div>
</div>
</div>
</div>
{/* Another Divider */}
<div>
<svg width="1136" height="2" viewBox="0 0 1136 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1H1136" stroke="var(--Text-Gray-200, #E9EAEB)" />
</svg>
</div>
{/* Action Buttons */}
<div className="w-[1175px] p-6 inline-flex justify-start items-center gap-2">
<div
onClick={handleReset}
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Reset</div>
</div>
</div>
<div
onClick={handleSaveChanges}
className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Brand-Orange]/90"
>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Save Changes</div>
</div>
</div>
</div>
</>
)}
{/* Billing Content */}
{activeTab === 'billing' && (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-semibold text-Text-Gray-800 mb-4">Plan & Billing</h2>
<p className="text-Text-Gray-500">Billing management features would be implemented here.</p>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default SettingsNew;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
import { initializeApp, getApps } from 'firebase/app';
import { getAuth, GoogleAuthProvider, setPersistence, browserLocalPersistence } from 'firebase/auth';
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
@@ -29,22 +28,8 @@ if (isFirebaseConfigured) {
app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
auth = getAuth(app);
setPersistence(auth, browserLocalPersistence);
db = getFirestore(app);
googleProvider = new GoogleAuthProvider();
// Connect to emulator in development
const isLocalhost = typeof window !== 'undefined' &&
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
if (isLocalhost && !db._settings?.host?.includes('localhost')) {
try {
connectFirestoreEmulator(db, 'localhost', 5003);
console.log('🔥 Connected to Firestore emulator on localhost:5003');
} catch (error) {
console.log('⚠️ Firestore emulator already connected or connection failed');
}
}
console.log('🔥 Firebase initialized successfully');
} else {
auth = null;

Binary file not shown.

View File

@@ -190,7 +190,7 @@ class SecureApiService {
}
// Submission Methods
async getSubmissions(): Promise<Submission[]> {
async getSubmissions(): Promise<GetSubmissions> {
const response = await this.makeRequest<{ submissions: Submission[] }>(
'getSubmissions'
);