Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ run_all:
run:
cd server/src && go install # Install first so that we keep cached build objects around.

set -a && . server/debug.env && set +a && \
set -a && \
. server/debug.env && \
{ [ -f .env ] && . ./.env || true; } && \
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should launch.json be updated too? 🙏:)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left this one: the Go Server debug config loads a single envFile (server/debug.env), and the new .env is optional (only needed for the Places key locally). Pointing the debugger at a .env most people won't have would break 'Go Server' for them. If you want Places in the debugger, easiest is to drop GOOGLE_PLACES_API_KEY into your local server/debug.env. I documented the .env route in the README.

(posted by claude)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so far makefile and launch.json behavior have been kept in sync and i'd like to keep it that way if possible, so we don't have situations where things work depending on how they're launched. perhaps we could have 'make deps' create the file if it's missing so it can be added to launch.json too?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you say how to populate .env in the README, or make a template env file and just mention it in the README? probably say this somewhere: "This API key can be found in GCP under the dev environment."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — added a note under the Google Places section of the README on populating a root .env, including that the key is in GCP under the dev environment.

(posted by claude)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that actually where the key you used is? I tried it and got this error:

Google Maps JavaScript API error: ApiNotActivatedMapError
https://developers.google.com/maps/documentation/javascript/error-messages#api-not-activated-map-error

Would be great to mention which project/key in the readme. I'd like to test it out so pls lmk :)

set +a && \
cd server/src && \
PORT=$(PORT) go run main.go

Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,20 @@ Please reach out to tech@dxe.io to get an API key to sign people up.

- IPGEOLOCATION_KEY

### Google Places API Key for finding city information on public-facing forms
### Google Places API Key for finding city information on public-facing forms and the events location picker

- GOOGLE_PLACES_API_KEY

For local development this key is not in `server/debug.env`. To use the location
picker locally, create a `.env` file in the repository root (it is loaded by
`make run` on top of `server/debug.env`) containing:

```
GOOGLE_PLACES_API_KEY=...
```
Comment thread
jakehobbs marked this conversation as resolved.

This API key can be found in GCP under the dev environment.

## Logging and Monitoring

In production, logs are collected in CloudWatch under a log group called `adb`.
Expand Down
4 changes: 3 additions & 1 deletion cli/cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ INSERT INTO activists
VALUES
(`+ch+`, 1000, 'nnn', 'test2@gmail.com', '', 'United States', 'Supporter', 'Petition: no-more-bad-things', '`+currentDateString(1)+`');

INSERT INTO events VALUES
INSERT INTO events
(id, name, date, event_type, survey_sent, suppress_survey, circle_id, chapter_id)
VALUES
%s

INSERT INTO event_attendance (activist_id, event_id) VALUES
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use client'

import Link from 'next/link'
import { useQuery } from '@tanstack/react-query'
import { format, parseISO } from 'date-fns'
import {
CheckCircle2,
Users,
Pencil,
CalendarPlus,
Home,
Clock,
MapPin,
Globe,
} from 'lucide-react'
import { API_PATH, apiClient } from '@/lib/api'
import { Button } from '@/components/ui/button'
import { formatEventTimeRange } from '@/lib/time'

export function EventConfirmation({ eventId }: { eventId: number }) {
// Hydrated by the server page (same query key as the event form), so this
// resolves immediately on first render.
const { data: event } = useQuery({
queryKey: [API_PATH.EVENT_GET, String(eventId)],
queryFn: ({ signal }) => apiClient.getEvent(eventId, signal),
})

const timeRange = event ? formatEventTimeRange(event) : ''
const locationLabel =
event?.location?.name || event?.location?.formatted_address

return (
<div className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-3 text-center">
<CheckCircle2 className="h-12 w-12 text-green-600" />
<div className="flex flex-col gap-1">
<h2 className="text-xl font-semibold">Event created</h2>
<p className="text-sm text-muted-foreground">
Your event is scheduled.
</p>
</div>
</div>

{event && (
<div className="flex flex-col gap-2 rounded-xl border bg-card p-5 shadow-sm">
<p className="text-base font-semibold">{event.event_name}</p>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-sm text-muted-foreground">
<span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs font-medium">
{event.event_type}
</span>
<span>{format(parseISO(event.event_date), 'PPP')}</span>
{timeRange && (
<span className="inline-flex items-center gap-1">
<Clock className="h-3.5 w-3.5" />
{timeRange}
</span>
)}
{event.is_online ? (
<span className="inline-flex items-center gap-1">
<Globe className="h-3.5 w-3.5" />
Online
</span>
) : (
locationLabel && (
<span className="inline-flex items-center gap-1">
<MapPin className="h-3.5 w-3.5" />
{locationLabel}
</span>
)
)}
</div>
</div>
)}

<div className="flex flex-col gap-2">
<Button asChild>
<Link href={`/events/${eventId}`}>
<Users className="h-4 w-4" />
Take attendance now
</Link>
</Button>
<Button asChild variant="outline">
<Link href={`/events/${eventId}?expanded=1`}>
<Pencil className="h-4 w-4" />
Edit event
</Link>
</Button>
<Button asChild variant="outline">
<Link href="/events/new">
<CalendarPlus className="h-4 w-4" />
Create another event
</Link>
</Button>
<Button asChild variant="ghost">
<Link href="/home">
<Home className="h-4 w-4" />
Done
</Link>
</Button>
</div>
</div>
)
}
45 changes: 45 additions & 0 deletions frontend-v2/src/app/(authed)/events/[id]/confirmation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
import { notFound } from 'next/navigation'
import { ContentWrapper } from '@/app/content-wrapper'
import { EventConfirmation } from './event-confirmation'
import { API_PATH, ApiClient } from '@/lib/api'
import { getCookies } from '@/lib/auth'
import { redirectForHttpError } from '@/lib/server-auth'

// Shown right after a scheduled (public) event is created. Attendance happens
// later, at the event, so we confirm the event is on the schedule and offer the
// natural next steps instead of dropping the user on the attendance page.
export default async function EventConfirmationPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const eventId = parseInt(id)
if (Number.isNaN(eventId)) {
notFound()
}

const apiClient = new ApiClient(await getCookies())
const queryClient = new QueryClient()

await redirectForHttpError(() =>
// Intentionally use fetchQuery instead of prefetchQuery; see redirectForHttpError for details.
queryClient.fetchQuery({
queryKey: [API_PATH.EVENT_GET, String(eventId)],
queryFn: ({ signal }) => apiClient.getEvent(eventId, signal),
}),
)

return (
<ContentWrapper size="sm" className="gap-8">
<HydrationBoundary state={dehydrate(queryClient)}>
<EventConfirmation eventId={eventId} />
</HydrationBoundary>
</ContentWrapper>
)
}
8 changes: 5 additions & 3 deletions frontend-v2/src/app/(authed)/events/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import { redirectForHttpError } from '@/lib/server-auth'

export default async function EditEventPage({
params,
searchParams,
}: {
params: Promise<{ id: string }>
searchParams: Promise<{ expanded?: string }>
}) {
const { id } = await params
const [{ id }, { expanded }] = await Promise.all([params, searchParams])
const eventId = parseInt(id)
if (Number.isNaN(eventId)) {
notFound()
Expand All @@ -35,8 +37,8 @@ export default async function EditEventPage({
return (
<ContentWrapper size="sm" className="gap-8">
<HydrationBoundary state={dehydrate(queryClient)}>
<h1 className="text-3xl font-bold">Attendance</h1>
<EventForm mode="event" />
<h1 className="text-3xl font-bold">Event</h1>
<EventForm mode="event" startExpanded={expanded === '1'} />
</HydrationBoundary>
</ContentWrapper>
)
Expand Down
76 changes: 76 additions & 0 deletions frontend-v2/src/app/(authed)/events/event-form-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { z } from 'zod'

export const EVENT_TYPES = [
'Action',
'Campaign Action',
'Community',
'Frontline Surveillance',
'Meeting',
'Outreach',
'Animal Care',
'Training',
] as const

export const DEFAULT_FIELD_COUNT = 5
export const MIN_EMPTY_FIELDS = 1

export type EventFormMode = 'event' | 'connection'

// Zod schema for form validation.
export const attendeeSchema = z.object({
name: z.string().refine(
(name) => {
const trimmed = name.trim()
// Empty is ok, as it will be filtered out.
if (trimmed === '') return true
// Must have at least first and last name (contains a space).
return trimmed.indexOf(' ') !== -1
},
{
message: 'First & last name are required',
},
),
})

export const formSchema = z
.object({
eventName: z.string().min(1, 'Event name is required'),
eventType: z.string().min(1, 'Event type is required'),
eventDate: z.string().min(1, 'Event date is required'),
suppressSurvey: z.boolean(),
attendees: z.array(attendeeSchema),
// Upcoming-event fields. Kept permissive here (empty string / false is
// valid) so the plain attendance form still passes; the stricter per-mode
// rules — a start time and a location for public events — are enforced by
// the refinements below.
isPublic: z.boolean(),
isOnline: z.boolean(),
description: z.string(),
startTime: z.string(),
endTime: z.string(),
timezone: z.string(),
googlePlaceId: z.string(),
locationName: z.string(),
formattedAddress: z.string(),
lat: z.number().optional(),
lng: z.number().optional(),
})
// TODO: events that cross midnight (end before start, e.g. an overnight
// vigil) can't be expressed yet — leave the end time blank for now. If this
// becomes a real need, add an explicit "end date" rather than inferring it.
.refine((v) => !(v.startTime && v.endTime) || v.endTime >= v.startTime, {
message: 'End time must be after start time',
path: ['endTime'],
})
// Publicly listed events are scheduled in advance, so a start time is required.
.refine((v) => !v.isPublic || Boolean(v.startTime), {
message: 'Start time is required for public events',
path: ['startTime'],
})
// In-person public events need a location; online ones don't.
.refine((v) => !v.isPublic || v.isOnline || Boolean(v.googlePlaceId), {
message: 'Location is required for in-person public events',
path: ['formattedAddress'],
})

export type FormValues = z.infer<typeof formSchema>
Loading
Loading