Skip to content

Commit

Permalink
Prefill user ID when replying to an opportunity
Browse files Browse the repository at this point in the history
  • Loading branch information
zoul committed Oct 31, 2024
1 parent 4b45bb4 commit 90a1d2a
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 22 deletions.
32 changes: 32 additions & 0 deletions app/opportunities/[slug]/ResponseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { useSession } from "next-auth/react";

import { SidebarCTA } from "~/components/Sidebar";
import { type Opportunity } from "~/src/data/opportunity";
import { assertIsOurUser } from "~/src/utils";

type Props = {
role: Pick<Opportunity, "responseUrl" | "prefillUserId">;
};

export const ResponseButton = ({ role }: Props) => {
const { data: session, status: sessionStatus } = useSession();
if (
role.prefillUserId &&
sessionStatus === "authenticated" &&
session.user &&
role.responseUrl.startsWith("https://")
) {
// If we have a valid session and the response URL is an ordinary
// HTTPS URL, add current user ID to the response URL.
assertIsOurUser(session.user);
const responseUrl = new URL(role.responseUrl);
responseUrl.searchParams.append("prefill_User", session.user.id);
responseUrl.searchParams.append("hide_User", "true");
return <SidebarCTA href={responseUrl.toString()} label="Mám zájem ✨" />;
} else {
// Otherwise just use the response URL from the database.
return <SidebarCTA href={role.responseUrl} label="Mám zájem" />;
}
};
5 changes: 3 additions & 2 deletions app/opportunities/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { type Metadata } from "next";
import Image from "next/image";
import { notFound } from "next/navigation";

import { ResponseButton } from "~/app/opportunities/[slug]/ResponseButton";
import { Breadcrumbs } from "~/components/Breadcrumbs";
import { ImageLabel, ProjectImageLabel } from "~/components/ImageLabel";
import { MarkdownContent } from "~/components/MarkdownContent";
import { OpportunityRow } from "~/components/OpportunityRow";
import { RelatedContent } from "~/components/RelatedContent";
import { Sidebar, SidebarCTA, SidebarSection } from "~/components/Sidebar";
import { Sidebar, SidebarSection } from "~/components/Sidebar";
import { getAllOpportunities, type Opportunity } from "~/src/data/opportunity";
import { getAllProjects, type Project } from "~/src/data/project";
import { getAlternativeOpenRoles } from "~/src/data/queries";
Expand Down Expand Up @@ -102,7 +103,7 @@ const RoleSidebar = ({
label={owner.name}
/>
</SidebarSection>
<SidebarCTA href={role.contactUrl} label="Mám zájem" />
<ResponseButton role={role} />
</Sidebar>
);

Expand Down
25 changes: 7 additions & 18 deletions src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import sendgrid from "@sendgrid/mail";
import {
getServerSession,
type DefaultSession,
type NextAuthOptions,
} from "next-auth";
import { getServerSession, type NextAuthOptions } from "next-auth";
import EmailProvider from "next-auth/providers/email";
import SlackProvider from "next-auth/providers/slack";
import { optional, record, string } from "typescript-json-decoder";
Expand All @@ -16,14 +12,12 @@ import {
logUnknownEmailSignInEvent,
} from "~/src/data/auth";
import { Route } from "~/src/routing";
import { devMode, isHttpSuccessCode } from "~/src/utils";

export type OurUser = {
id: string;
name: string;
email: string;
image: string;
};
import {
assertIsOurUser,
devMode,
isHttpSuccessCode,
type OurUser,
} from "~/src/utils";

/** NextAuth options used to configure various parts of the authentication machinery */
export const authOptions: NextAuthOptions = {
Expand Down Expand Up @@ -92,11 +86,6 @@ export const authOptions: NextAuthOptions = {
// The session callback is called whenever a session is checked.
// https://next-auth.js.org/configuration/callbacks#session-callback
async session({ session, token }) {
function assertIsOurUser(
user: DefaultSession["user"],
): asserts user is OurUser {
/* If there is a user it’s always OurUser */
}
if (session.user) {
assertIsOurUser(session.user);
// Expose our user ID to the client side
Expand Down
3 changes: 2 additions & 1 deletion src/data/opportunity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ test("Decode opportunity", () => {
summary: { source: "- Práce na **mobilní** _aplikaci_." },
timeRequirements: "3–5 hodin týdně",
ownerId: "rec0ABdJtGIK9AeCB",
contactUrl: "https://cesko-digital.slack.com/archives/C01AENB1LPP",
responseUrl: "https://cesko-digital.slack.com/archives/C01AENB1LPP",
prefillUserId: false,
creationTime: "2021-09-02T17:20:26.000Z",
coverImageUrl:
"https://data.cesko.digital/web/projects/loono/cover-loono.jpg",
Expand Down
5 changes: 4 additions & 1 deletion src/data/opportunity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
array,
boolean,
field,
optional,
record,
Expand All @@ -14,6 +15,7 @@ import {
decodeValidItemsFromArray,
markdown,
takeFirst,
withDefault,
} from "~/src/decoding";

import { appBase, unwrapRecords } from "./airtable";
Expand All @@ -33,7 +35,8 @@ export const decodeOpportunity = record({
summary: field("Summary", markdown),
timeRequirements: field("Time Requirements", string),
ownerId: field("Owner ID", takeFirst(array(string))),
contactUrl: field("RSVP URL", decodeUrl),
responseUrl: field("RSVP URL", decodeUrl),
prefillUserId: field("Prefill User ID", withDefault(boolean, false)),
coverImageUrl: field("Cover URL", optional(string)),
skills: field("Skills", decodeSkills),
status: field("Status", union("draft", "live", "unlisted")),
Expand Down
21 changes: 21 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import crypto from "crypto";

import Markdoc, { type Node } from "@markdoc/markdoc";
import { type DefaultSession } from "next-auth";

/** Default user avatar picture URL if we have no better one */
export const defaultAvatarUrl =
Expand Down Expand Up @@ -190,3 +191,23 @@ export const devMode = () => process.env.NODE_ENV === "development";

/** Is given HTTP status code a successful one? */
export const isHttpSuccessCode = (code: number) => 200 <= code && code < 300;

/**
* The type of user data we keep in the session
*
* TBD: It would be much better to setup NextAuth to operate with this type
* everywhere without having to cast.
*/
export type OurUser = {
id: string;
name: string;
email: string;
image: string;
};

/** Utility assert to make TypeScript believe the user value in session is `OurUser` */
export function assertIsOurUser(
user: DefaultSession["user"],
): asserts user is OurUser {
/* If there is a user it’s always OurUser */
}

0 comments on commit 90a1d2a

Please sign in to comment.