evaldoc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.staging.example +19 -0
- package/.firebaserc +6 -0
- package/.github/workflows/auto-merge-to-main.yml +28 -0
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/npxhub-publish.yml +170 -0
- package/.prettierignore +4 -0
- package/.prettierrc.json +7 -0
- package/README.md +142 -0
- package/SRS.md +448 -0
- package/apphosting.staging.yaml +48 -0
- package/apphosting.yaml +50 -0
- package/bin/evaldoc.cjs +105 -0
- package/cors.json +8 -0
- package/docs/ARCHITECTURE.md +769 -0
- package/docs/ARCHITECTURE_SUMMARY.md +323 -0
- package/docs/DEPLOYMENT.md +86 -0
- package/docs/DEPLOYMENT_STEPS.md +373 -0
- package/docs/DEPLOY_FIRESTORE_RULES.md +239 -0
- package/docs/DEPLOY_STORAGE_RULES.md +82 -0
- package/docs/DOCTOR_TUTORIAL.md +467 -0
- package/docs/FIREBASE_STORAGE_CORS_FIX.md +131 -0
- package/docs/FIXES_APPLIED.md +128 -0
- package/docs/IMPLEMENTATION.md +498 -0
- package/docs/IMPLEMENTATION_COMPLETE.md +317 -0
- package/docs/LLM-SETUP.md +216 -0
- package/docs/MANUAL_TEST_GUIDE.md +327 -0
- package/docs/PATIENT_GUIDE.md +451 -0
- package/docs/PLATFORM_OVERVIEW.md +194 -0
- package/docs/QUICKSTART.md +252 -0
- package/docs/QUICK_REFERENCE.md +222 -0
- package/docs/READY_TO_TEST.md +383 -0
- package/docs/RUN.md +237 -0
- package/docs/START_TESTING.md +130 -0
- package/docs/STRIPE_CONNECT_SETUP.md +454 -0
- package/docs/SYSTEM_READY.md +345 -0
- package/docs/THREADS_IMPLEMENTATION_COMPLETE.md +431 -0
- package/docs/THREAD_IMPLEMENTATION_GUIDE.md +345 -0
- package/docs/firebasefiles.md +21 -0
- package/docs/homepage.md +1208 -0
- package/docs/payments.md +159 -0
- package/eslint.config.js +76 -0
- package/firebase.json +27 -0
- package/firestore.indexes.json +88 -0
- package/firestore.rules +125 -0
- package/functions/package-lock.json +2884 -0
- package/functions/package.json +20 -0
- package/functions/src/analytics.js +101 -0
- package/functions/src/audit.js +28 -0
- package/functions/src/auth.js +19 -0
- package/functions/src/drafts.js +128 -0
- package/functions/src/feedback.js +164 -0
- package/functions/src/firebase-init.js +29 -0
- package/functions/src/i18n.js +134 -0
- package/functions/src/limits.js +27 -0
- package/functions/src/llm.js +271 -0
- package/functions/src/ner.js +162 -0
- package/functions/src/notifications.js +273 -0
- package/functions/src/orgs.js +127 -0
- package/functions/src/patient-api.js +545 -0
- package/functions/src/payment.js +830 -0
- package/functions/src/safety.js +188 -0
- package/functions/src/stripe-config.js +33 -0
- package/functions/src/topics.js +94 -0
- package/functions/src/triage.js +191 -0
- package/functions/src/utils.js +28 -0
- package/functions/src/validate.js +177 -0
- package/functions/test-firebase.js +36 -0
- package/jest.config.js +18 -0
- package/package.json +52 -0
- package/public/404.html +93 -0
- package/public/app/analytics.html +336 -0
- package/public/app/analytics.js +177 -0
- package/public/app/app.css +1400 -0
- package/public/app/app.js +1754 -0
- package/public/app/config.js +14 -0
- package/public/app/inbox-threads.js +843 -0
- package/public/app/inbox.html +598 -0
- package/public/app/inbox.js +397 -0
- package/public/app/index.html +612 -0
- package/public/assets/logo.jpg +0 -0
- package/public/chat.html +137 -0
- package/public/css/chat.css +870 -0
- package/public/css/landing-enhance.css +298 -0
- package/public/css/styles.css +1198 -0
- package/public/favicon.svg +5 -0
- package/public/find-doctor.html +220 -0
- package/public/index.html +399 -0
- package/public/js/chat-threads.js +513 -0
- package/public/js/chat.js +832 -0
- package/public/js/search.js +41 -0
- package/public/privacy.html +101 -0
- package/public/robots.txt +6 -0
- package/public/site.webmanifest +12 -0
- package/public/terms.html +110 -0
- package/scripts/create-test-provider.js +55 -0
- package/scripts/deploy-staging.sh +51 -0
- package/scripts/fix-provider.js +39 -0
- package/scripts/quick-test.sh +37 -0
- package/scripts/test-chat.sh +54 -0
- package/scripts/test-multi-turn.js +137 -0
- package/scripts/test-ner-simple.js +28 -0
- package/scripts/test-ner.js +60 -0
- package/scripts/test-stripe-connect.js +304 -0
- package/scripts/test-webhook-secret.sh +24 -0
- package/server.js +3864 -0
- package/storage.rules +22 -0
- package/test-api.json +1 -0
- package/tests/emulator/README.md +38 -0
- package/tests/emulator/counters.emulator.test.js +81 -0
- package/tests/integration/api-account-selfserve.test.js +312 -0
- package/tests/integration/api-admin-digest.test.js +202 -0
- package/tests/integration/api-chat-satisfaction.test.js +259 -0
- package/tests/integration/api-doctor-drafts.test.js +236 -0
- package/tests/integration/api-doctor-inbox.test.js +401 -0
- package/tests/integration/api-org.test.js +328 -0
- package/tests/integration/api-payment.test.js +664 -0
- package/tests/integration/api-provider-settings.test.js +475 -0
- package/tests/integration/api-qr.test.js +121 -0
- package/tests/integration/api-security.test.js +241 -0
- package/tests/integration/api-stripe-search.test.js +303 -0
- package/tests/integration/api-webhook-deletion.test.js +231 -0
- package/tests/mocks/firebase-admin.js +220 -0
- package/tests/unit/analytics.test.js +151 -0
- package/tests/unit/audit-bugs.test.js +203 -0
- package/tests/unit/chat-ui-logic.test.js +283 -0
- package/tests/unit/drafts.test.js +104 -0
- package/tests/unit/feedback.test.js +148 -0
- package/tests/unit/i18n.test.js +229 -0
- package/tests/unit/llm-retry.test.js +122 -0
- package/tests/unit/notifications.test.js +155 -0
- package/tests/unit/orgs.test.js +328 -0
- package/tests/unit/payment.test.js +889 -0
- package/tests/unit/safety-edge-cases.test.js +509 -0
- package/tests/unit/safety-srs-compliance.test.js +252 -0
- package/tests/unit/safety.test.js +291 -0
- package/tests/unit/server-helpers.test.js +774 -0
- package/tests/unit/topics-limits.test.js +158 -0
- package/tests/unit/triage.test.js +152 -0
- package/tests/unit/validate.test.js +408 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, dependency-free input-validation module for EvalDoc.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes input validation to reduce the unvalidated-input bug class.
|
|
5
|
+
* No I/O, no Date.now/Math.random — fully deterministic and side-effect free.
|
|
6
|
+
*
|
|
7
|
+
* Primary export: validate(rules, input) — schema-driven validation/coercion.
|
|
8
|
+
* Helper exports: isEmail, clampInt, isHexColor, boundedString.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Reasonable email regex: local@domain.tld, no spaces, single @, dotted domain.
|
|
12
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
13
|
+
const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
|
14
|
+
|
|
15
|
+
/** True if str is a valid-looking email (after trim + lowercase). */
|
|
16
|
+
function isEmail(str) {
|
|
17
|
+
if (typeof str !== 'string') return false;
|
|
18
|
+
return EMAIL_REGEX.test(str.trim().toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** True if str is a 6-digit hex color like #aabbcc. */
|
|
22
|
+
function isHexColor(str) {
|
|
23
|
+
return typeof str === 'string' && HEX_COLOR_REGEX.test(str);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse val to an integer and clamp into [min, max].
|
|
28
|
+
* Returns fallback when val is not a finite integer-parseable value.
|
|
29
|
+
*/
|
|
30
|
+
function clampInt(val, min, max, fallback) {
|
|
31
|
+
const n = parseInt(val, 10);
|
|
32
|
+
if (!Number.isFinite(n)) return fallback;
|
|
33
|
+
let out = n;
|
|
34
|
+
if (typeof min === 'number' && out < min) out = min;
|
|
35
|
+
if (typeof max === 'number' && out > max) out = max;
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Coerce to string, trim, and truncate to maxLen characters. */
|
|
40
|
+
function boundedString(str, maxLen) {
|
|
41
|
+
let s = String(str == null ? '' : str).trim();
|
|
42
|
+
if (typeof maxLen === 'number' && s.length > maxLen) s = s.slice(0, maxLen);
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate and coerce an input object against a rules schema.
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} rules field name -> rule object
|
|
50
|
+
* rule: { type, required?, min?, max?, maxLength?, values?, default? }
|
|
51
|
+
* type: 'string'|'email'|'int'|'number'|'boolean'|'enum'|'hexColor'
|
|
52
|
+
* @param {Object} input raw input object
|
|
53
|
+
* @returns {{ ok: boolean, errors: Object, value: Object }}
|
|
54
|
+
*/
|
|
55
|
+
function validate(rules, input) {
|
|
56
|
+
const errors = {};
|
|
57
|
+
const value = {};
|
|
58
|
+
const src = input && typeof input === 'object' ? input : {};
|
|
59
|
+
|
|
60
|
+
for (const field of Object.keys(rules || {})) {
|
|
61
|
+
const rule = rules[field] || {};
|
|
62
|
+
const present = Object.prototype.hasOwnProperty.call(src, field) &&
|
|
63
|
+
src[field] !== undefined && src[field] !== null;
|
|
64
|
+
|
|
65
|
+
// Absent field: apply default, error if required, else omit.
|
|
66
|
+
if (!present) {
|
|
67
|
+
if (Object.prototype.hasOwnProperty.call(rule, 'default')) {
|
|
68
|
+
value[field] = rule.default;
|
|
69
|
+
} else if (rule.required) {
|
|
70
|
+
errors[field] = `${field} is required`;
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const raw = src[field];
|
|
76
|
+
|
|
77
|
+
switch (rule.type) {
|
|
78
|
+
case 'string': {
|
|
79
|
+
let s = boundedString(raw, rule.maxLength);
|
|
80
|
+
if (s === '' && rule.required) {
|
|
81
|
+
errors[field] = `${field} is required`;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (typeof rule.min === 'number' && s.length < rule.min) {
|
|
85
|
+
errors[field] = `${field} must be at least ${rule.min} characters`;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (typeof rule.max === 'number' && s.length > rule.max) {
|
|
89
|
+
errors[field] = `${field} must be at most ${rule.max} characters`;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
value[field] = s;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case 'email': {
|
|
97
|
+
const e = String(raw).trim().toLowerCase();
|
|
98
|
+
if (!isEmail(e)) {
|
|
99
|
+
errors[field] = `${field} must be a valid email`;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
value[field] = e;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'int': {
|
|
107
|
+
const n = parseInt(raw, 10);
|
|
108
|
+
if (!Number.isFinite(n)) {
|
|
109
|
+
if (Object.prototype.hasOwnProperty.call(rule, 'default')) {
|
|
110
|
+
value[field] = rule.default;
|
|
111
|
+
} else {
|
|
112
|
+
errors[field] = `${field} must be an integer`;
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
value[field] = clampInt(n, rule.min, rule.max, n);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case 'number': {
|
|
121
|
+
const n = Number(raw);
|
|
122
|
+
if (!Number.isFinite(n)) {
|
|
123
|
+
if (Object.prototype.hasOwnProperty.call(rule, 'default')) {
|
|
124
|
+
value[field] = rule.default;
|
|
125
|
+
} else {
|
|
126
|
+
errors[field] = `${field} must be a number`;
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
let out = n;
|
|
131
|
+
if (typeof rule.min === 'number' && out < rule.min) out = rule.min;
|
|
132
|
+
if (typeof rule.max === 'number' && out > rule.max) out = rule.max;
|
|
133
|
+
value[field] = out;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'boolean': {
|
|
138
|
+
if (typeof raw === 'boolean') {
|
|
139
|
+
value[field] = raw;
|
|
140
|
+
} else if (rule.required) {
|
|
141
|
+
errors[field] = `${field} must be a boolean`;
|
|
142
|
+
}
|
|
143
|
+
// non-boolean & optional -> ignored (omitted)
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case 'enum': {
|
|
148
|
+
const allowed = Array.isArray(rule.values) ? rule.values : [];
|
|
149
|
+
if (allowed.includes(raw)) {
|
|
150
|
+
value[field] = raw;
|
|
151
|
+
} else if (Object.prototype.hasOwnProperty.call(rule, 'default')) {
|
|
152
|
+
value[field] = rule.default;
|
|
153
|
+
} else {
|
|
154
|
+
errors[field] = `${field} must be one of: ${allowed.join(', ')}`;
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'hexColor': {
|
|
160
|
+
if (isHexColor(raw)) {
|
|
161
|
+
value[field] = raw;
|
|
162
|
+
} else {
|
|
163
|
+
errors[field] = `${field} must be a hex color like #rrggbb`;
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
default:
|
|
169
|
+
errors[field] = `${field} has unknown type "${rule.type}"`;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { ok: Object.keys(errors).length === 0, errors, value };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { validate, isEmail, clampInt, isHexColor, boundedString };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Quick test to check Firebase connectivity
|
|
2
|
+
const admin = require('firebase-admin');
|
|
3
|
+
|
|
4
|
+
// Initialize with project ID
|
|
5
|
+
admin.initializeApp({
|
|
6
|
+
projectId: 'evaldocplatform'
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const db = admin.firestore();
|
|
10
|
+
|
|
11
|
+
async function testConnection() {
|
|
12
|
+
try {
|
|
13
|
+
console.log('Testing Firebase connection...');
|
|
14
|
+
|
|
15
|
+
// Try to read from providers collection
|
|
16
|
+
const snapshot = await db.collection('providers').limit(1).get();
|
|
17
|
+
console.log('✓ Successfully connected to Firestore');
|
|
18
|
+
console.log(`Found ${snapshot.size} provider(s)`);
|
|
19
|
+
|
|
20
|
+
if (!snapshot.empty) {
|
|
21
|
+
const doc = snapshot.docs[0];
|
|
22
|
+
console.log('Sample provider:', {
|
|
23
|
+
id: doc.id,
|
|
24
|
+
handle: doc.data().handle,
|
|
25
|
+
displayName: doc.data().displayName
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
process.exit(0);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('✗ Error connecting to Firebase:', error.message);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
testConnection();
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
testMatch: ['**/tests/**/*.test.js'],
|
|
4
|
+
// Emulator tests (*.emulator.test.js under tests/emulator/) require a running
|
|
5
|
+
// Firestore emulator and are excluded from the default `jest` run. Run them via
|
|
6
|
+
// `firebase emulators:exec` (see tests/emulator/README.md).
|
|
7
|
+
testPathIgnorePatterns: ['/node_modules/', '/tests/emulator/'],
|
|
8
|
+
collectCoverageFrom: [
|
|
9
|
+
'functions/src/**/*.js',
|
|
10
|
+
'server.js',
|
|
11
|
+
'!**/node_modules/**'
|
|
12
|
+
],
|
|
13
|
+
testTimeout: 15000,
|
|
14
|
+
verbose: true
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Set NODE_ENV for test guard in server.js
|
|
18
|
+
process.env.NODE_ENV = 'test';
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "evaldoc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "EvalDoc Healthcare Chat Platform - Simple Node.js App",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node server.js",
|
|
8
|
+
"dev": "nodemon server.js",
|
|
9
|
+
"test": "jest --forceExit --detectOpenHandles",
|
|
10
|
+
"test:unit": "jest tests/unit --forceExit",
|
|
11
|
+
"test:integration": "jest tests/integration --forceExit --detectOpenHandles",
|
|
12
|
+
"test:coverage": "jest --coverage --forceExit --detectOpenHandles",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint . --fix",
|
|
15
|
+
"format": "prettier --write .",
|
|
16
|
+
"format:check": "prettier --check ."
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@google/generative-ai": "^0.24.1",
|
|
23
|
+
"@huggingface/inference": "^4.13.5",
|
|
24
|
+
"cors": "^2.8.5",
|
|
25
|
+
"dotenv": "^17.2.3",
|
|
26
|
+
"express": "^4.18.2",
|
|
27
|
+
"express-rate-limit": "^8.5.2",
|
|
28
|
+
"firebase-admin": "^12.2.0",
|
|
29
|
+
"openai": "^6.15.0",
|
|
30
|
+
"qrcode": "^1.5.4",
|
|
31
|
+
"stripe": "^20.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"eslint": "^9.13.0",
|
|
35
|
+
"globals": "^15.11.0",
|
|
36
|
+
"jest": "^30.2.0",
|
|
37
|
+
"nodemon": "^3.0.2",
|
|
38
|
+
"prettier": "^3.3.3",
|
|
39
|
+
"supertest": "^7.2.2"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"healthcare",
|
|
43
|
+
"chat",
|
|
44
|
+
"firebase",
|
|
45
|
+
"ai"
|
|
46
|
+
],
|
|
47
|
+
"author": "EvalDoc Team",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"bin": {
|
|
50
|
+
"evaldoc": "bin/evaldoc.cjs"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/public/404.html
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Page Not Found - EvalDoc</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<style>
|
|
14
|
+
:root {
|
|
15
|
+
--bg-dark: #0e1a14;
|
|
16
|
+
--bg-mid: #1b3a2b;
|
|
17
|
+
--bg-light: #f1efe7;
|
|
18
|
+
--accent: #f4b860;
|
|
19
|
+
--accent-2: #4fb3a4;
|
|
20
|
+
--text: #0b0b0b;
|
|
21
|
+
}
|
|
22
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
23
|
+
body {
|
|
24
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
25
|
+
background: var(--bg-light);
|
|
26
|
+
color: var(--text);
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
}
|
|
32
|
+
.container {
|
|
33
|
+
text-align: center;
|
|
34
|
+
padding: 2rem;
|
|
35
|
+
max-width: 480px;
|
|
36
|
+
}
|
|
37
|
+
.error-code {
|
|
38
|
+
font-size: 6rem;
|
|
39
|
+
font-weight: 700;
|
|
40
|
+
color: var(--bg-mid);
|
|
41
|
+
line-height: 1;
|
|
42
|
+
margin-bottom: 0.5rem;
|
|
43
|
+
}
|
|
44
|
+
h1 {
|
|
45
|
+
font-size: 1.5rem;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
margin-bottom: 1rem;
|
|
48
|
+
color: var(--bg-dark);
|
|
49
|
+
}
|
|
50
|
+
p {
|
|
51
|
+
color: #666;
|
|
52
|
+
margin-bottom: 2rem;
|
|
53
|
+
line-height: 1.6;
|
|
54
|
+
}
|
|
55
|
+
.actions {
|
|
56
|
+
display: flex;
|
|
57
|
+
gap: 1rem;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
flex-wrap: wrap;
|
|
60
|
+
}
|
|
61
|
+
a {
|
|
62
|
+
display: inline-block;
|
|
63
|
+
padding: 0.75rem 1.5rem;
|
|
64
|
+
border-radius: 8px;
|
|
65
|
+
text-decoration: none;
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
font-size: 0.95rem;
|
|
68
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
69
|
+
}
|
|
70
|
+
a:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
|
71
|
+
.btn-primary {
|
|
72
|
+
background: var(--bg-mid);
|
|
73
|
+
color: #fff;
|
|
74
|
+
}
|
|
75
|
+
.btn-secondary {
|
|
76
|
+
background: #fff;
|
|
77
|
+
color: var(--bg-dark);
|
|
78
|
+
border: 1px solid #ddd;
|
|
79
|
+
}
|
|
80
|
+
</style>
|
|
81
|
+
</head>
|
|
82
|
+
<body>
|
|
83
|
+
<div class="container">
|
|
84
|
+
<div class="error-code">404</div>
|
|
85
|
+
<h1>Provider not found</h1>
|
|
86
|
+
<p>The link you followed doesn't match any provider on EvalDoc. It may have been removed, or the URL might be incorrect.</p>
|
|
87
|
+
<div class="actions">
|
|
88
|
+
<a href="/" class="btn-primary">Go to homepage</a>
|
|
89
|
+
<a href="/find-doctor.html" class="btn-secondary">Find a provider</a>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|