Added missing bird

This commit is contained in:
Corey Blais
2026-04-17 17:11:11 -04:00
parent 328a9a704d
commit e06dae91a3
6 changed files with 366 additions and 4 deletions
+106
View File
@@ -176,6 +176,13 @@ type AuthFormState = {
email: string;
};
type LostBirdReportFormState = {
tagId: string;
finderEmail: string;
foundLocation: string;
message: string;
};
type AuthNotice = {
message: string;
previewUrl?: string | null;
@@ -280,6 +287,13 @@ const emptyAuthForm: AuthFormState = {
email: '',
};
const emptyLostBirdReportForm: LostBirdReportFormState = {
tagId: '',
finderEmail: '',
foundLocation: '',
message: '',
};
const emptyIntegrationTokenForm: IntegrationTokenFormState = {
name: '',
scope: 'read_write',
@@ -867,6 +881,9 @@ function App() {
const [authNotice, setAuthNotice] = useState<AuthNotice | null>(null);
const [authLoading, setAuthLoading] = useState(true);
const [authSubmitting, setAuthSubmitting] = useState(false);
const [lostBirdReportForm, setLostBirdReportForm] = useState<LostBirdReportFormState>(emptyLostBirdReportForm);
const [lostBirdReportNotice, setLostBirdReportNotice] = useState<{ message: string; kind: 'success' | 'error' } | null>(null);
const [lostBirdReportSubmitting, setLostBirdReportSubmitting] = useState(false);
const [workspace, setWorkspace] = useState<Workspace | null>(null);
const [activeMembership, setActiveMembership] = useState<WorkspaceMember | null>(null);
const [workspaceMembers, setWorkspaceMembers] = useState<WorkspaceMember[]>([]);
@@ -1533,6 +1550,43 @@ function App() {
}
};
const handleLostBirdReportSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLostBirdReportNotice(null);
setLostBirdReportSubmitting(true);
try {
const response = await apiFetch('/lost-bird/report', undefined, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tagId: lostBirdReportForm.tagId.trim(),
finderEmail: lostBirdReportForm.finderEmail.trim(),
foundLocation: lostBirdReportForm.foundLocation.trim(),
message: lostBirdReportForm.message.trim(),
}),
});
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Unable to send this report right now.'));
}
const data = (await readJsonSafely<{ message?: string }>(response)) ?? {};
setLostBirdReportNotice({
message: data.message ?? 'Report received.',
kind: 'success',
});
setLostBirdReportForm(emptyLostBirdReportForm);
} catch (reportError) {
setLostBirdReportNotice({
message: reportError instanceof Error ? reportError.message : 'Unable to send this report right now.',
kind: 'error',
});
} finally {
setLostBirdReportSubmitting(false);
}
};
const handleLogout = async () => {
setError('');
@@ -2610,6 +2664,57 @@ function App() {
<p className="muted">
Keep every bird's care story in one place; your flock's health, history, and routines together and easier to visualize.
</p>
<details className="summary-card lost-bird-login-card">
<summary>
<span>
<span className="eyebrow">Report a missing bird</span>
</span>
</summary>
<p className="muted">Enter the band ID and FlockPal will notify the flock if that bird is in the system.</p>
<form className="form-panel" onSubmit={handleLostBirdReportSubmit}>
<label>
Bird band ID
<input
value={lostBirdReportForm.tagId}
onChange={(event) => setLostBirdReportForm({ ...lostBirdReportForm, tagId: event.target.value })}
placeholder="Example: ABC-123"
required
/>
</label>
<label>
Your email
<input
type="email"
value={lostBirdReportForm.finderEmail}
onChange={(event) => setLostBirdReportForm({ ...lostBirdReportForm, finderEmail: event.target.value })}
placeholder="Optional, but helpful"
/>
</label>
<label>
Where was the bird found?
<input
value={lostBirdReportForm.foundLocation}
onChange={(event) => setLostBirdReportForm({ ...lostBirdReportForm, foundLocation: event.target.value })}
placeholder="City, neighborhood, or nearby landmark"
/>
</label>
<label>
Message for the flock
<textarea
value={lostBirdReportForm.message}
onChange={(event) => setLostBirdReportForm({ ...lostBirdReportForm, message: event.target.value })}
placeholder="Add any safe details that could help the flock contact you."
rows={3}
/>
</label>
<button className="primary-button" type="submit" disabled={lostBirdReportSubmitting}>
{lostBirdReportSubmitting ? 'Sending report...' : 'Notify the flock'}
</button>
</form>
{lostBirdReportNotice ? (
<p className={lostBirdReportNotice.kind === 'error' ? 'error-banner' : 'success-banner'}>{lostBirdReportNotice.message}</p>
) : null}
</details>
</div>
<div className="auth-card">
@@ -2676,6 +2781,7 @@ function App() {
</a>
))}
</div>
</div>
</section>
</main>
+61 -2
View File
@@ -152,7 +152,7 @@ textarea {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 1.5rem;
align-items: start;
align-items: stretch;
}
.auth-hero-card {
@@ -167,7 +167,10 @@ textarea {
}
.auth-copy {
align-content: start;
position: relative;
align-content: stretch;
grid-template-rows: auto auto auto 1fr auto;
padding-bottom: 3.75rem;
}
.auth-card {
@@ -177,6 +180,53 @@ textarea {
border: 1px solid var(--panel-border);
}
.lost-bird-login-card {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
gap: 0.65rem;
padding: 0.75rem 0.85rem;
}
.lost-bird-login-card[open] {
box-shadow: 0 24px 48px rgba(86, 63, 34, 0.24);
}
.lost-bird-login-card summary {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
list-style: none;
}
.lost-bird-login-card summary::-webkit-details-marker {
display: none;
}
.lost-bird-login-card summary::after {
content: "Open";
border: 1px solid rgba(39, 105, 179, 0.18);
border-radius: 999px;
padding: 0.45rem 0.75rem;
background: rgba(255, 254, 250, 0.72);
color: var(--accent-green);
font-size: 0.82rem;
font-weight: 800;
}
.lost-bird-login-card[open] summary::after {
content: "Close";
}
.lost-bird-login-card .eyebrow {
display: block;
margin-bottom: 0;
}
.auth-illustration-card {
margin: 0;
padding: 1rem;
@@ -1192,6 +1242,15 @@ label {
color: #922728;
}
.success-banner {
margin: 0;
padding: 1rem 1.2rem;
border-radius: 18px;
background: rgba(35, 138, 90, 0.1);
border: 1px solid rgba(35, 138, 90, 0.2);
color: var(--accent-green);
}
.picker-list {
display: flex;
gap: 0.75rem;