incognitolm commited on
Commit
99df9a4
·
1 Parent(s): 64c17f7
data/contacts.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
package.json CHANGED
@@ -1,3 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  {
2
  "name": "tanstack_start_ts",
3
  "private": true,
 
1
+ {
2
+ "name": "safesight-web",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "start:server": "node server.js"
7
+ },
8
+ "dependencies": {
9
+ "express": "^4.18.2",
10
+ "cors": "^2.8.5"
11
+ }
12
+ }
13
  {
14
  "name": "tanstack_start_ts",
15
  "private": true,
server.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const cors = require('cors');
5
+
6
+ const app = express();
7
+ app.use(cors());
8
+ app.use(express.json());
9
+
10
+ const dataDir = path.join(__dirname, 'data');
11
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
12
+
13
+ const contactsFile = path.join(dataDir, 'contacts.json');
14
+ if (!fs.existsSync(contactsFile)) fs.writeFileSync(contactsFile, '[]');
15
+
16
+ app.get('/data/contacts', (req, res) => {
17
+ try {
18
+ const raw = fs.readFileSync(contactsFile, 'utf8');
19
+ const json = JSON.parse(raw || '[]');
20
+ res.json(json);
21
+ } catch (err) {
22
+ console.error(err);
23
+ res.status(500).json({ error: 'failed to read contacts' });
24
+ }
25
+ });
26
+
27
+ app.post('/data/contacts', (req, res) => {
28
+ try {
29
+ const body = req.body || {};
30
+ const entry = {
31
+ name: body.name || '',
32
+ email: body.email || '',
33
+ subject: body.subject || '',
34
+ message: body.message || '',
35
+ createdAt: new Date().toISOString(),
36
+ };
37
+ const raw = fs.readFileSync(contactsFile, 'utf8');
38
+ const arr = JSON.parse(raw || '[]');
39
+ arr.unshift(entry);
40
+ fs.writeFileSync(contactsFile, JSON.stringify(arr, null, 2));
41
+ res.status(201).json(entry);
42
+ } catch (err) {
43
+ console.error(err);
44
+ res.status(500).json({ error: 'failed to save contact' });
45
+ }
46
+ });
47
+
48
+ const port = process.env.PORT || 5173;
49
+ app.listen(port, () => console.log(`Data server listening on http://localhost:${port}`));
src/routes/admin/contacts.tsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { useEffect, useState } from "react";
3
+ import { Card, CardContent } from "@/components/ui/card";
4
+
5
+ export const Route = createFileRoute("/admin")({
6
+ head: () => ({
7
+ meta: [
8
+ { title: "Admin — Contact Submissions" },
9
+ { name: "robots", content: "noindex" },
10
+ ],
11
+ }),
12
+ component: AdminContactsPage,
13
+ });
14
+
15
+ function AdminContactsPage() {
16
+ const [items, setItems] = useState<Array<any>>([]);
17
+
18
+ async function load() {
19
+ try {
20
+ const res = await fetch('/data/contacts');
21
+ if (!res.ok) throw new Error('failed to fetch');
22
+ const json = await res.json();
23
+ setItems(json || []);
24
+ } catch (err) {
25
+ console.error(err);
26
+ // fallback to localStorage
27
+ try {
28
+ const key = 'safesight:contact_submissions';
29
+ const existing = JSON.parse(localStorage.getItem(key) || '[]');
30
+ setItems(existing);
31
+ } catch (e) {
32
+ setItems([]);
33
+ }
34
+ }
35
+ }
36
+
37
+ useEffect(() => {
38
+ load();
39
+ }, []);
40
+
41
+ return (
42
+ <div className="flex min-h-screen flex-col">
43
+ <section className="bg-navy py-8">
44
+ <div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
45
+ <h1 className="text-2xl font-bold text-navy-foreground">Admin: Contact Submissions</h1>
46
+ <p className="mt-2 text-sm text-navy-foreground/80">No password required. Anyone with this page URL may view submissions.</p>
47
+ </div>
48
+ </section>
49
+
50
+ <section className="bg-background py-10">
51
+ <div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
52
+ <div className="flex justify-between items-center mb-4">
53
+ <h2 className="text-lg font-semibold text-foreground">Submissions ({items.length})</h2>
54
+ <button onClick={load} className="text-sm text-accent hover:underline">Refresh</button>
55
+ </div>
56
+
57
+ <div className="space-y-4">
58
+ {items.length === 0 ? (
59
+ <Card className="border-0 bg-card shadow-sm">
60
+ <CardContent className="p-6">No submissions yet.</CardContent>
61
+ </Card>
62
+ ) : (
63
+ items.map((it, i) => (
64
+ <Card key={i} className="border-0 bg-card shadow-sm">
65
+ <CardContent className="p-6">
66
+ <div className="flex justify-between">
67
+ <div>
68
+ <p className="font-medium text-foreground">{it.subject}</p>
69
+ <p className="mt-1 text-sm text-muted-foreground">From: {it.name} — {it.email}</p>
70
+ </div>
71
+ <div className="text-sm text-muted-foreground">{it.createdAt ? new Date(it.createdAt).toLocaleString() : "—"}</div>
72
+ </div>
73
+ <div className="mt-4 text-sm text-foreground whitespace-pre-wrap">{it.message}</div>
74
+ </CardContent>
75
+ </Card>
76
+ ))
77
+ )}
78
+ </div>
79
+ </div>
80
+ </section>
81
+ </div>
82
+ );
83
+ }
src/routes/contact.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import { createFileRoute } from "@tanstack/react-router";
 
2
  import { Button } from "@/components/ui/button";
3
  import { Card, CardContent } from "@/components/ui/card";
4
  import { Input } from "@/components/ui/input";
@@ -54,6 +55,48 @@ const faqs = [
54
  ];
55
 
56
  function ContactPage() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  return (
58
  <div className="flex min-h-screen flex-col">
59
  {/* Hero */}
@@ -108,11 +151,29 @@ function ContactPage() {
108
  <Card className="border-0 bg-card shadow-sm">
109
  <CardContent className="p-6 sm:p-8">
110
  <h2 className="text-xl font-semibold text-foreground">Send us a message</h2>
 
 
 
 
 
111
  <form
112
  className="mt-6 space-y-4"
113
  onSubmit={(e) => {
114
  e.preventDefault();
115
- alert("Thanks for reaching out! We'll get back to you soon.");
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  }}
117
  >
118
  <div className="grid gap-4 sm:grid-cols-2">
@@ -120,20 +181,23 @@ function ContactPage() {
120
  <label htmlFor="name" className="block text-sm font-medium text-foreground">
121
  Name
122
  </label>
123
- <Input id="name" placeholder="Your name" className="mt-1" />
 
124
  </div>
125
  <div>
126
  <label htmlFor="email" className="block text-sm font-medium text-foreground">
127
  Email
128
  </label>
129
- <Input id="email" type="email" placeholder="you@example.com" className="mt-1" />
 
130
  </div>
131
  </div>
132
  <div>
133
  <label htmlFor="subject" className="block text-sm font-medium text-foreground">
134
  Subject
135
  </label>
136
- <Input id="subject" placeholder="What's this about?" className="mt-1" />
 
137
  </div>
138
  <div>
139
  <label htmlFor="message" className="block text-sm font-medium text-foreground">
@@ -143,11 +207,18 @@ function ContactPage() {
143
  id="message"
144
  placeholder="Tell us how we can help..."
145
  className="mt-1 min-h-[120px]"
 
 
 
146
  />
 
 
 
 
 
 
 
147
  </div>
148
- <Button type="submit" className="bg-primary text-primary-foreground hover:bg-primary/90">
149
- Send Message
150
- </Button>
151
  </form>
152
  </CardContent>
153
  </Card>
 
1
  import { createFileRoute } from "@tanstack/react-router";
2
+ import { useState } from "react";
3
  import { Button } from "@/components/ui/button";
4
  import { Card, CardContent } from "@/components/ui/card";
5
  import { Input } from "@/components/ui/input";
 
55
  ];
56
 
57
  function ContactPage() {
58
+ const [name, setName] = useState("");
59
+ const [email, setEmail] = useState("");
60
+ const [subject, setSubject] = useState("");
61
+ const [message, setMessage] = useState("");
62
+ const [errors, setErrors] = useState<{ [k: string]: string }>({});
63
+ const [success, setSuccess] = useState<string | null>(null);
64
+
65
+ function validate() {
66
+ const e: { [k: string]: string } = {};
67
+ if (!name.trim()) e.name = "Name is required.";
68
+ if (!email.trim()) e.email = "Email is required.";
69
+ else if (!/^\S+@\S+\.\S+$/.test(email)) e.email = "Enter a valid email.";
70
+ if (!subject.trim()) e.subject = "Subject is required.";
71
+ if (!message.trim()) e.message = "Message is required.";
72
+ return e;
73
+ }
74
+
75
+ async function saveSubmission() {
76
+ const payload = { name, email, subject, message };
77
+ try {
78
+ const res = await fetch('/data/contacts', {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify(payload),
82
+ });
83
+ if (!res.ok) throw new Error('server error');
84
+ return true;
85
+ } catch (err) {
86
+ // fallback to localStorage if server unavailable
87
+ try {
88
+ const key = 'safesight:contact_submissions';
89
+ const existing = JSON.parse(localStorage.getItem(key) || '[]');
90
+ existing.unshift({ ...payload, createdAt: new Date().toISOString() });
91
+ localStorage.setItem(key, JSON.stringify(existing));
92
+ return false;
93
+ } catch (e) {
94
+ console.error('Failed to save submission', e);
95
+ return false;
96
+ }
97
+ }
98
+ }
99
+
100
  return (
101
  <div className="flex min-h-screen flex-col">
102
  {/* Hero */}
 
151
  <Card className="border-0 bg-card shadow-sm">
152
  <CardContent className="p-6 sm:p-8">
153
  <h2 className="text-xl font-semibold text-foreground">Send us a message</h2>
154
+ <p className="mt-2 text-sm text-muted-foreground">
155
+ Warning: Submissions are visible to anyone with the admin link. Do not include passwords, SSNs, or other sensitive personal data.
156
+ </p>
157
+ {/* Admin page is intentionally unlinked; access at /admin only. */}
158
+
159
  <form
160
  className="mt-6 space-y-4"
161
  onSubmit={(e) => {
162
  e.preventDefault();
163
+ setSuccess(null);
164
+ const v = validate();
165
+ setErrors(v);
166
+ if (Object.keys(v).length === 0) {
167
+ const ok = await saveSubmission();
168
+ setSuccess("Thanks for reaching out! We'll get back to you soon.");
169
+ setName("");
170
+ setEmail("");
171
+ setSubject("");
172
+ setMessage("");
173
+ if (!ok) {
174
+ // optional: inform user that submission is stored locally
175
+ }
176
+ }
177
  }}
178
  >
179
  <div className="grid gap-4 sm:grid-cols-2">
 
181
  <label htmlFor="name" className="block text-sm font-medium text-foreground">
182
  Name
183
  </label>
184
+ <Input id="name" placeholder="Your name" className="mt-1" value={name} onChange={(e) => setName(e.currentTarget.value)} required />
185
+ {errors.name ? <p className="mt-1 text-xs text-destructive">{errors.name}</p> : null}
186
  </div>
187
  <div>
188
  <label htmlFor="email" className="block text-sm font-medium text-foreground">
189
  Email
190
  </label>
191
+ <Input id="email" type="email" placeholder="you@example.com" className="mt-1" value={email} onChange={(e) => setEmail(e.currentTarget.value)} required />
192
+ {errors.email ? <p className="mt-1 text-xs text-destructive">{errors.email}</p> : null}
193
  </div>
194
  </div>
195
  <div>
196
  <label htmlFor="subject" className="block text-sm font-medium text-foreground">
197
  Subject
198
  </label>
199
+ <Input id="subject" placeholder="What's this about?" className="mt-1" value={subject} onChange={(e) => setSubject(e.currentTarget.value)} required />
200
+ {errors.subject ? <p className="mt-1 text-xs text-destructive">{errors.subject}</p> : null}
201
  </div>
202
  <div>
203
  <label htmlFor="message" className="block text-sm font-medium text-foreground">
 
207
  id="message"
208
  placeholder="Tell us how we can help..."
209
  className="mt-1 min-h-[120px]"
210
+ value={message}
211
+ onChange={(e) => setMessage(e.currentTarget.value)}
212
+ required
213
  />
214
+ {errors.message ? <p className="mt-1 text-xs text-destructive">{errors.message}</p> : null}
215
+ </div>
216
+ <div className="flex items-center gap-4">
217
+ <Button type="submit" className="bg-primary text-primary-foreground hover:bg-primary/90">
218
+ Send Message
219
+ </Button>
220
+ {success ? <p className="text-sm text-success">{success}</p> : null}
221
  </div>
 
 
 
222
  </form>
223
  </CardContent>
224
  </Card>
src/routes/why-safesight.tsx CHANGED
@@ -30,13 +30,13 @@ export const Route = createFileRoute("/why-safesight")({
30
  });
31
 
32
  const comparisonFeatures = [
33
- { feature: "Real-time gaze tracking", safesight: true, other: false },
34
- { feature: "On-device processing (no cloud)", safesight: true, other: false },
35
- { feature: "Instant audio + visual alerts", safesight: true, other: false },
36
- { feature: "Focus score after every trip", safesight: true, other: false },
37
- { feature: "Drowsiness detection", safesight: true, other: false },
38
- { feature: "Privacy-first design", safesight: true, other: false },
39
- { feature: "Affordable subscription", safesight: true, other: false },
40
  ];
41
 
42
  const benefits = [
@@ -148,10 +148,13 @@ function WhySafeSightPage() {
148
  )}
149
  </td>
150
  <td className="px-6 py-4 text-center">
151
- {row.other ? (
152
- <CheckCircle2 className="mx-auto h-5 w-5 text-muted-foreground" />
153
- ) : (
154
- <XCircle className="mx-auto h-5 w-5 text-muted-foreground" />
 
 
 
155
  )}
156
  </td>
157
  </tr>
 
30
  });
31
 
32
  const comparisonFeatures = [
33
+ { feature: "Real-time gaze tracking", safesight: true, other: "no" },
34
+ { feature: "On-device processing (no cloud)", safesight: true, other: "some" },
35
+ { feature: "Instant audio + visual alerts", safesight: true, other: "some" },
36
+ { feature: "Focus score after every trip", safesight: true, other: "no" },
37
+ { feature: "Drowsiness detection", safesight: true, other: "some" },
38
+ { feature: "Privacy-first design", safesight: true, other: "some" },
39
+ { feature: "Affordable subscription option", safesight: true, other: "some" },
40
  ];
41
 
42
  const benefits = [
 
148
  )}
149
  </td>
150
  <td className="px-6 py-4 text-center">
151
+ {row.other === "yes" && <CheckCircle2 className="mx-auto h-5 w-5 text-muted-foreground" />}
152
+ {row.other === "no" && <XCircle className="mx-auto h-5 w-5 text-muted-foreground" />}
153
+ {row.other === "some" && (
154
+ <div className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
155
+ <AlertTriangle className="h-4 w-4" />
156
+ <span>Varies</span>
157
+ </div>
158
  )}
159
  </td>
160
  </tr>