additional fetaure buildout for footer and footer links
This commit is contained in:
@@ -12,6 +12,7 @@ import AlertHistory from "./pages/alertHistory/AlertHistory";
|
||||
import Account from "./pages/user_account/Account";
|
||||
import Terms from "./pages/terms/Terms";
|
||||
import Privacy from "./pages/privacy/Privacy";
|
||||
import Support from "./pages/support/Support";
|
||||
|
||||
export default function App() {
|
||||
const { currentUser, isSubscribed } = useAuth();
|
||||
@@ -38,6 +39,7 @@ export default function App() {
|
||||
<Route path="/account" element={<><Navbar /><Account /></>} />
|
||||
<Route path="/terms" element={<><Navbar /><Terms /></>} />
|
||||
<Route path="/privacy" element={<><Navbar /><Privacy /></>} />
|
||||
<Route path="/support" element={<><Navbar /><Support /></>} />
|
||||
<Route path="*" element={<Navigate to="/subscribe" />} />
|
||||
</Routes>
|
||||
</div>
|
||||
@@ -60,6 +62,7 @@ export default function App() {
|
||||
<Route path="/subscribe/return/:sessionId" element={<CheckoutReturn />} />
|
||||
<Route path="/terms" element={<Terms />} />
|
||||
<Route path="/privacy" element={<Privacy />} />
|
||||
<Route path="/support" element={<Support />} />
|
||||
<Route path="*" element={<Navigate to="/addresses" />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
23
frontend/src/api/support.js
Normal file
23
frontend/src/api/support.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getAuthHeaders } from "./authHeaders";
|
||||
import { API_BASE } from "./config";
|
||||
|
||||
export async function submitSupportRequest({ email, description }) {
|
||||
const headers = await getAuthHeaders();
|
||||
const res = await fetch(`${API_BASE}/support`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ email, description }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
let message = "Failed to send support request";
|
||||
try {
|
||||
const data = await res.json();
|
||||
if (data.message) message = data.message;
|
||||
else if (data.error && res.status) message = `${data.error} (${res.status})`;
|
||||
} catch {
|
||||
message = `Request failed (${res.status}). Is the API running on port 3001?`;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
@@ -27,14 +27,14 @@ export default function Footer() {
|
||||
>
|
||||
Privacy
|
||||
</Link>
|
||||
<a
|
||||
href="mailto:support@koinp.ing"
|
||||
<Link
|
||||
to="/support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="footer__link"
|
||||
>
|
||||
Contact Support
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
31
frontend/src/pages/support/Support.css
Normal file
31
frontend/src/pages/support/Support.css
Normal file
@@ -0,0 +1,31 @@
|
||||
.support-page__title {
|
||||
font-size: 2rem;
|
||||
font-weight: 200;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.support-page__intro {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 200;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.support-page__alert {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.support-form__description {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.support-form__textarea {
|
||||
min-height: 10rem;
|
||||
resize: vertical;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.support-form__submit {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
121
frontend/src/pages/support/Support.jsx
Normal file
121
frontend/src/pages/support/Support.jsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import Input from "../../components/Input";
|
||||
import Button from "../../components/Button";
|
||||
import { submitSupportRequest } from "../../api/support";
|
||||
import "./Support.css";
|
||||
|
||||
const EMAIL_MAX = 320;
|
||||
const DESCRIPTION_MAX = 8000;
|
||||
const DESCRIPTION_PATTERN = /^[a-zA-Z0-9\s]+$/;
|
||||
|
||||
function validateEmail(value) {
|
||||
const v = value.trim();
|
||||
if (!v) return "Email is required.";
|
||||
if (v.length > EMAIL_MAX) return "Email is too long.";
|
||||
const ok =
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/.test(
|
||||
v,
|
||||
);
|
||||
if (!ok) return "Enter a valid email address.";
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateDescription(value) {
|
||||
const t = value.trim();
|
||||
if (!t) return "Description of issue is required.";
|
||||
if (t.length > DESCRIPTION_MAX) return "Description is too long.";
|
||||
if (!DESCRIPTION_PATTERN.test(t)) {
|
||||
return "Use only letters, numbers, and spaces (no special characters).";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function Support() {
|
||||
const { currentUser } = useAuth();
|
||||
const [email, setEmail] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [error, setError] = useState(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser?.email) {
|
||||
setEmail(currentUser.email);
|
||||
}
|
||||
}, [currentUser?.email]);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
const emailErr = validateEmail(email);
|
||||
if (emailErr) {
|
||||
setError(emailErr);
|
||||
return;
|
||||
}
|
||||
const descErr = validateDescription(description);
|
||||
if (descErr) {
|
||||
setError(descErr);
|
||||
return;
|
||||
}
|
||||
|
||||
setSending(true);
|
||||
try {
|
||||
await submitSupportRequest({
|
||||
email: email.trim(),
|
||||
description: description.trim(),
|
||||
});
|
||||
setSuccess(true);
|
||||
setDescription("");
|
||||
} catch (err) {
|
||||
setError(err.message || "Something went wrong.");
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="support-page page">
|
||||
<h1 className="support-page__title">Contact support</h1>
|
||||
<p className="support-page__intro">
|
||||
Describe your issue below. We will reply to the email address you
|
||||
provide.
|
||||
</p>
|
||||
|
||||
{error && <div className="alert alert--error support-page__alert">{error}</div>}
|
||||
{success && (
|
||||
<div className="alert alert--success support-page__alert">
|
||||
Your message was sent. Thank you.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="support-form" onSubmit={handleSubmit} noValidate>
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
autoComplete="email"
|
||||
required
|
||||
/>
|
||||
<label className="form-field support-form__description">
|
||||
<div className="input__label">Description of Issue</div>
|
||||
<textarea
|
||||
className="form-control support-form__textarea"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={8}
|
||||
maxLength={DESCRIPTION_MAX}
|
||||
placeholder="Letters, numbers, and spaces only"
|
||||
aria-required="true"
|
||||
/>
|
||||
</label>
|
||||
<Button type="submit" disabled={sending} className="support-form__submit">
|
||||
{sending ? "Sending…" : "Send"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -22,3 +22,8 @@
|
||||
.tos-para:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tos-para strong {
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Terms() {
|
||||
TO ACCESS OR USE THE WEB SITE OR APPS.
|
||||
</p>
|
||||
<p className="tos-para">
|
||||
Communications. You agree that by providing your contact information,
|
||||
<strong>Communications.</strong> You agree that by providing your contact information,
|
||||
you consent to receiving communication, in connection with your
|
||||
Membership subscription. This may include communication about your
|
||||
account, features, and services via e-email, push notification, phone,
|
||||
@@ -38,7 +38,7 @@ export default function Terms() {
|
||||
charges applied by your cell phone carrier may apply.
|
||||
</p>
|
||||
<p className="tos-para">
|
||||
User Eligibility. The Website should only be accessed and used by
|
||||
<strong>User Eligibility.</strong> The Website should only be accessed and used by
|
||||
individuals who agree to be bound by these Terms of Use and who are at
|
||||
least 18 years of age. The Websites may be accessible worldwide;
|
||||
however, the Websites are intended for use only in the USA and Canada.
|
||||
@@ -48,7 +48,7 @@ export default function Terms() {
|
||||
Websites.
|
||||
</p>
|
||||
<p className="tos-para">
|
||||
Intellectual property rights. The Web Site and the information, computer
|
||||
<strong>Intellectual property rights.</strong> The Web Site and the information, computer
|
||||
code, and related functionality appearing, featured or otherwise
|
||||
displayed on the Websites are owned by Koin Ping, its affiliates, and
|
||||
their respective licensors or other third parties and protected under
|
||||
@@ -56,7 +56,7 @@ export default function Terms() {
|
||||
countries sand international treaty provisions.
|
||||
</p>
|
||||
<p className="tos-para">
|
||||
Limited license. Koin Ping grants to you a limited, non-exclusive,
|
||||
<strong>Limited license.</strong> Koin Ping grants to you a limited, non-exclusive,
|
||||
non-transferable license to use the Web Site in strict accordance with
|
||||
these Terms of Service and Use and the instructions provided by us on
|
||||
the Web Site. The materials provided on the Web Site, including,
|
||||
@@ -77,7 +77,7 @@ export default function Terms() {
|
||||
publicity and communications regulations and statutes.
|
||||
</p>
|
||||
<p className="tos-para">
|
||||
Restrictions on Use. As a condition of your use of the Websites, you
|
||||
<strong>Restrictions on Use.</strong> As a condition of your use of the Websites, you
|
||||
warrant that you will not use the Websites for any purpose that is
|
||||
unlawful or prohibited by these terms, conditions and notices. You may
|
||||
not use the Websites in any way that could damage, disable, overburden
|
||||
@@ -87,7 +87,7 @@ export default function Terms() {
|
||||
available or provided for through the Web Site.
|
||||
</p>
|
||||
<p className="tos-para">
|
||||
Revocation of privileges. You agree that your use of the Websites may
|
||||
<strong>Revocation of privileges.</strong> You agree that your use of the Websites may
|
||||
be suspended or terminated immediately upon receipt of any notice which
|
||||
alleges that you have used the Websites in violation of these Terms of
|
||||
Use and/or for any purpose that violates any local, state, federal or
|
||||
@@ -102,7 +102,7 @@ export default function Terms() {
|
||||
protect our rights.
|
||||
</p>
|
||||
<p className="tos-para">
|
||||
No Warranties. Koin Ping MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT
|
||||
<strong>No Warranties.</strong> Koin Ping MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT
|
||||
THE WEB SITE, THE SUITABILITY OF THE INFORMATION CONTAINED ON OR
|
||||
RECEIVED THROUGH THE WEB SITE, OR ANY SERVICES OR PRODUCTS RECEIVED
|
||||
THROUGH THE WEB SITE. ALL INFORMATION AND USE OF THE WEB SITE ARE
|
||||
@@ -122,7 +122,7 @@ export default function Terms() {
|
||||
REPRESENTATIONS OR STATEMENTS OTHER THAN IN THIS AGREEMENT.
|
||||
</p>
|
||||
<p className="tos-para">
|
||||
Limitation of Liability. UNDER NO CIRCUMSTANCES SHALL Koin Ping BE
|
||||
<strong>Limitation of Liability.</strong> UNDER NO CIRCUMSTANCES SHALL Koin Ping BE
|
||||
LIABLE FOR ANY DAMAGES, INCLUDING, WITHOUT LIMITATION, DIRECT,
|
||||
INDIRECT, PUNITIVE, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES OR LOST
|
||||
PROFITS THAT RESULT FROM, OR ARISE OUT OF OR IN CONNECTION WITH THE USE
|
||||
|
||||
Reference in New Issue
Block a user