Skip to content

Commit

Permalink
Annotate flight-contextual tasting status on flight details
Browse files Browse the repository at this point in the history
Adjust the hasTasted attribute to be contextual, and tweak flight details to deemphasize already recorded tastings.
  • Loading branch information
dcramer committed Dec 21, 2023
1 parent 859b7c1 commit b8a392d
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 50 deletions.
50 changes: 36 additions & 14 deletions apps/server/src/serializers/bottle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { and, eq, getTableColumns, inArray, sql } from "drizzle-orm";
import { type z } from "zod";
import { serialize, serializer } from ".";
import { db } from "../db";
import type { Bottle, User } from "../db/schema";
import type { Bottle, Flight, User } from "../db/schema";
import {
bottlesToDistillers,
collectionBottles,
Expand All @@ -14,19 +14,26 @@ import { notEmpty } from "../lib/filter";
import { type BottleSchema } from "../schemas";
import { EntitySerializer } from "./entity";

type TastingAttrs = {
type Attrs = {
isFavorite: boolean;
hasTasted: boolean;
brand: ReturnType<(typeof EntitySerializer)["item"]>;
distillers: ReturnType<(typeof EntitySerializer)["item"]>[];
bottler: ReturnType<(typeof EntitySerializer)["item"]> | null;
};

type Context =
| {
flight?: Flight | null;
}
| undefined;

export const BottleSerializer = serializer({
attrs: async (
itemList: Bottle[],
currentUser?: User,
): Promise<Record<number, TastingAttrs>> => {
context?: Context,
): Promise<Record<number, Attrs>> => {
const itemIds = itemList.map((t) => t.id);

const distillerList = await db
Expand Down Expand Up @@ -89,18 +96,32 @@ export const BottleSerializer = serializer({
)
: new Set();

// identify bottles which have a tasting recorded for the current user
// note: this is contextual based on the query - if they're asking for a flight,
// said flight should be available in the context and this will reflect
// if they've recorded a tasting _in that context_
const tastedSet = currentUser
? new Set(
(
await db
.selectDistinct({ id: tastings.bottleId })
.from(tastings)
.where(
and(
inArray(tastings.bottleId, itemIds),
eq(tastings.createdById, currentUser.id),
),
)
(context?.flight
? await db
.selectDistinct({ id: tastings.bottleId })
.from(tastings)
.where(
and(
inArray(tastings.bottleId, itemIds),
eq(tastings.flightId, context.flight.id),
eq(tastings.createdById, currentUser.id),
),
)
: await db
.selectDistinct({ id: tastings.bottleId })
.from(tastings)
.where(
and(
inArray(tastings.bottleId, itemIds),
eq(tastings.createdById, currentUser.id),
),
)
).map((r) => r.id),
)
: new Set();
Expand All @@ -123,8 +144,9 @@ export const BottleSerializer = serializer({

item: (
item: Bottle,
attrs: TastingAttrs,
attrs: Attrs,
currentUser?: User,
context?: Context,
): z.infer<typeof BottleSchema> => {
return {
id: item.id,
Expand Down
48 changes: 36 additions & 12 deletions apps/server/src/serializers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ export type Attrs = Record<number, Record<string, any>>;
export interface Serializer<
T extends Item = Item,
R extends Record<string, any> = Record<string, any>,
C extends Record<string, any> = Record<string, any>,
A extends Record<string, any> = Record<string, any>,
> {
attrs?(itemList: T[], currentUser?: User | null): Promise<Record<number, A>>;
item(item: T, attrs: A, currentUser?: User | null): R;
attrs?(
itemList: T[],
currentUser?: User | null,
context?: C,
): Promise<Record<number, A>>;
item(item: T, attrs: A, currentUser?: User | null, context?: C): R;
}

export async function DefaultAttrs<T extends Item>(
Expand All @@ -22,45 +27,64 @@ export async function DefaultAttrs<T extends Item>(
return Object.fromEntries(itemList.map((i) => [i.id, {}]));
}

export async function serialize<T extends Item, R extends Record<string, any>>(
serializer: Serializer<T, R>,
export async function serialize<
T extends Item,
R extends Record<string, any>,
C extends Record<string, any>,
>(
serializer: Serializer<T, R, C>,
item: T,
currentUser?: User | null,
excludeFields?: string[],
context?: C,
): Promise<R>;
export async function serialize<T extends Item, R extends Record<string, any>>(
serializer: Serializer<T, R>,
export async function serialize<
T extends Item,
R extends Record<string, any>,
C extends Record<string, any>,
>(
serializer: Serializer<T, R, C>,
itemList: T[],
currentUser?: User | null,
excludeFields?: string[],
context?: C,
): Promise<R[]>;
export async function serialize<T extends Item, R extends Record<string, any>>(
serializer: Serializer<T, R>,
export async function serialize<
T extends Item,
R extends Record<string, any>,
C extends Record<string, any>,
>(
serializer: Serializer<T, R, C>,
itemList: T | T[],
currentUser?: User | null,
excludeFields: string[] = [],
context?: C,
): Promise<R | R[]> {
if (Array.isArray(itemList) && !itemList.length) return [];

const attrs = await (serializer.attrs || DefaultAttrs<T>)(
Array.isArray(itemList) ? itemList : [itemList],
currentUser,
context,
);

const results = (Array.isArray(itemList) ? itemList : [itemList]).map(
(i: T) =>
removeAttributes(
serializer.item(i, attrs[i.id] || {}, currentUser),
serializer.item(i, attrs[i.id] || {}, currentUser, context),
excludeFields,
),
) as R[];

return Array.isArray(itemList) ? results : results[0];
}

export function serializer<T extends Item, R extends Record<string, any>>(
v: Serializer<T, R>,
) {
export function serializer<
T extends Item,
R extends Record<string, any>,
C extends Record<string, any>,
A extends Record<string, any> = Record<string, any>,
>(v: Serializer<T, R, C, A>) {
return v;
}

Expand Down
25 changes: 10 additions & 15 deletions apps/server/src/serializers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const UserSerializer = serializer({
);
},
item: (item: User, attrs, currentUser): z.infer<typeof UserSchema> => {
const data = {
return {
id: item.id,
displayName: item.displayName,
username: item.username,
Expand All @@ -50,20 +50,15 @@ export const UserSerializer = serializer({
friendStatus:
attrs.friendStatus === "following" ? "friends" : attrs.friendStatus,
private: item.private,
};

if (
currentUser &&
...(currentUser &&
(currentUser.admin || currentUser.mod || currentUser.id === item.id)
) {
return {
...data,
email: item.email,
createdAt: item.createdAt.toISOString(),
admin: item.admin,
mod: item.admin || item.mod,
};
}
return data;
? {
email: item.email,
createdAt: item.createdAt.toISOString(),
admin: item.admin,
mod: item.admin || item.mod,
}
: {}),
};
},
});
9 changes: 8 additions & 1 deletion apps/server/src/trpc/routes/bottleList.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CATEGORY_LIST } from "@peated/server/constants";
import { db } from "@peated/server/db";
import type {
Flight} from "@peated/server/db/schema";
import {
bottles,
bottlesToDistillers,
Expand Down Expand Up @@ -115,8 +117,10 @@ export default publicProcedure
sql`EXISTS(SELECT 1 FROM ${tastings} WHERE ${input.tag} = ANY(${tastings.tags}) AND ${tastings.bottleId} = ${bottles.id})`,
);
}

let flight: Flight | null = null;
if (input.flight) {
const [flight] = await db
[flight] = await db
.select()
.from(flights)
.where(eq(flights.publicId, input.flight));
Expand Down Expand Up @@ -177,6 +181,9 @@ export default publicProcedure
results.slice(0, limit).map((r) => r.bottles),
ctx.user,
["description", "tastingNotes"],
{
flight,
},
),
rel: {
nextCursor: results.length > limit ? cursor + 1 : null,
Expand Down
8 changes: 4 additions & 4 deletions apps/web/app/components/pageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { ElementType, ReactNode } from "react";

export default function PageHeader({
metadata,
icon: Icon,
title,
titleExtra,
metadata,
icon: Icon,
}: {
metadata?: ReactNode;
icon?: ElementType;
title: string;
titleExtra?: ReactNode;
metadata?: ReactNode;
icon?: ElementType;
}) {
return (
<div className="my-4 flex w-full flex-wrap justify-center gap-x-3 gap-y-4 lg:flex-nowrap lg:justify-start">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/routes/flights.$flightId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export default function FlightDetails() {
</td>
<td className="py-4 pl-3 pr-4 text-right text-sm sm:table-cell sm:pr-3">
<Button
color="highlight"
color={bottle.hasTasted ? "default" : "highlight"}
size="small"
to={`/bottles/${bottle.id}/addTasting?flight=${flight.id}`}
>
Expand Down
13 changes: 10 additions & 3 deletions apps/web/app/routes/flights._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import Button from "@peated/web/components/button";
import EmptyActivity from "@peated/web/components/emptyActivity";
import Layout from "@peated/web/components/layout";
import ListItem from "@peated/web/components/listItem";
import SimpleHeader from "@peated/web/components/simpleHeader";
import { type MetaFunction } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { json } from "@remix-run/server-runtime";
import { type SitemapFunction } from "remix-sitemap";
import PageHeader from "../components/pageHeader";
import { redirectToAuth } from "../lib/auth";
import { makeIsomorphicLoader } from "../lib/isomorphicLoader";

Expand Down Expand Up @@ -41,15 +41,22 @@ export default function Flights() {

return (
<Layout>
<SimpleHeader>Flights</SimpleHeader>
<PageHeader
title="Flights"
metadata={
<Button color="primary" to="/addFlight">
Add Flight
</Button>
}
/>
<ul className="divide-y divide-slate-800 sm:rounded">
{results.length ? (
results.map((flight) => {
return (
<ListItem key={flight.id} as={Link} to={`/flights/${flight.id}`}>
<div className="flex flex-auto items-center space-x-4">
<div className="flex-auto space-y-1 font-medium group-hover:underline">
{flight.name}
{flight.name || <em>unknown flight</em>}
</div>
{flight.description && (
<div className="text-light text-sm">
Expand Down

0 comments on commit b8a392d

Please sign in to comment.