Skip to content

Commit

Permalink
feat: New session list previews.
Browse files Browse the repository at this point in the history
These are styled now as cards, with buttons for favoriting &
sharing (not yet hooked up, as the backend lacks support for this).

I have some concerns about how busy the UI is now, but I think this is
better than the alternatives I could come up with, which are:

1. Hide favoriting and sharing behind a long press, which it appears
Apple do not want to support on mobile Safari:
#188 (comment)

2. Single-icon favorite and share buttons, but these are not
particularly informative since they don't have any explanatory text.

I'm open to option 2, as they would take up less screen space and
there would be fewer words on the page, but it's not as discoverable
as the solution implemented here, which includes the words "Favorite"
and "Share" along with the iconic representations. We could support
both, and only display the icons in a more advanced mode, once the
student has some experience using the application.

Note that this commit introduces a new style of button, a sort of
built-in style for cards. I played around with our standard `UIButton`
instead of this new style, as I'm reluctant to add new UI design
language in general; but I think in this case, because we're adding a
major new UI element in the cards themselves, that it makes sense that
they have their own button style. The `UIButton` looked awful in this
context, IMO.
  • Loading branch information
dhess committed Dec 7, 2022
1 parent d18b362 commit 4b31801
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 20 deletions.
31 changes: 27 additions & 4 deletions src/components/SessionList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,34 @@ export interface SessionListProps {
}

export const SessionList = ({ sessions }: SessionListProps): JSX.Element => (
<ul className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
<ul
role="list"
className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"
>
{sessions.map((session) => (
<li key={session.id} className="relative">
<SessionPreview session={session} />
</li>
// Note: we would prefer to put a <li> here and use it to wrap the
// `SessionPreview`, implementing `SessionPreview` as a generic <div>.
// However, this breaks the CSS layout when any of the `SessionPreview`s'
// dates don't fit on a single line, as then the cards' heights vary
// depending on whether the displayed date wraps.
//
// However, if we implement the `SessionPreview` as a <li> rather than as
// a <div> inside a <li>, then the CSS works as expected: each card in a
// given row is the same height, irrespective of whether the displayed
// date wraps. (@dhess: I don't understand CSS well enough to know why
// this happens.)
//
// There are two implications of this design choice:
//
// 1. `SessionPreview` is less generic than we'd like (<li> rather than
// <div>).
//
// 2. We have to pass a `key` property here to `SessionPreview`, or else
// eslint complains, even though `SessionPreview` knows how to set the key
// itself. We could tell eslint to ignore the warning in this situation,
// but I'm not sure whether React also needs the key to be specified
// explicitly, so better safe than sorry.
<SessionPreview key={session.id} session={session} />
))}
</ul>
);
Expand Down
91 changes: 76 additions & 15 deletions src/components/SessionPreview/index.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,96 @@
import "@/index.css";

import { StarIcon, TrashIcon, UserPlusIcon } from "@heroicons/react/24/outline";

import type { Key } from "react";
import { Link } from "react-router-dom";

import { SessionMeta } from "@/Types";
import { BinaryTreePlaceholder } from "@/components";

export interface SessionPreviewProps {
session: SessionMeta;
key: Key;
}

// Note: `SessionPreview` is implemented as a <li> for reasons explained in
// `SessionList.ts`. This is fine for now, as we only display session previews
// in the sessions list, but we would prefer that this type were more generic.

export const SessionPreview = ({
session,
key,
}: SessionPreviewProps): JSX.Element => {
const locale: string = navigator.language;
return (
<div>
<Link to={`/sessions/${session.id}`} key={session.id}>
<div className="group flex w-full max-w-md flex-row justify-center overflow-hidden rounded-lg bg-grey-primary">
<BinaryTreePlaceholder className="h-16 w-16 fill-current text-white-primary group-hover:text-blue-primary md:h-48 md:w-48" />
<li
key={key}
className="col-span-1 flex flex-col divide-y divide-grey-quaternary rounded-lg bg-white-primary text-center drop-shadow-md"
>
<div className="flex flex-1 flex-col">
<Link
to={`/sessions/${session.id}`}
key={session.id}
className="group rounded-t-lg hover:text-blue-primary"
>
<BinaryTreePlaceholder className="mx-auto h-16 w-16 shrink-0 fill-current text-white-primary group-hover:text-blue-primary md:h-48 md:w-48" />
</Link>
<h3 className="mt-6 truncate font-medium text-blue-primary">
{session.name}
</h3>
<dl className="mt-1 flex grow flex-col justify-between">
<dt className="sr-only">Last modified</dt>
<dd className="text-xs text-blue-primary">
{new Intl.DateTimeFormat(locale, {
dateStyle: "medium",
timeStyle: "medium",
}).format(session.lastModified)}
</dd>
<dt className="sr-only">Tags</dt>
<dd className="mt-4">{/* Placeholder for tags. */}</dd>
</dl>
</div>
<div>
<div className="-mt-px flex divide-x divide-grey-quaternary">
<div className="-mr-px flex w-0 flex-1">
<button
type="button"
className="group relative inline-flex flex-1 items-center justify-center rounded-bl-lg border border-transparent py-4 text-sm font-medium text-blue-secondary hover:bg-blue-secondary hover:text-yellow-primary"
>
<StarIcon
className="h-5 w-5 fill-white-primary group-hover:fill-yellow-primary"
aria-hidden="true"
/>
<div className="sr-only">Favorite</div>
</button>
</div>
<div className="flex w-0 flex-1">
<button
type="button"
className="group relative inline-flex flex-1 items-center justify-center border border-transparent py-4 text-sm font-medium text-blue-secondary hover:bg-blue-secondary hover:text-green-primary"
>
<UserPlusIcon
className="h-5 w-5 fill-white-primary group-hover:fill-green-primary"
aria-hidden="true"
/>
<div className="sr-only">Share</div>
</button>
</div>
<div className="-ml-px flex w-0 flex-1">
<button
type="button"
className="group relative inline-flex flex-1 items-center justify-center rounded-br-lg border border-transparent py-4 text-sm font-medium text-red-secondary hover:bg-red-primary hover:text-white-primary"
>
<TrashIcon
className="h-5 w-5 fill-white-primary hover:stroke-red-primary group-hover:stroke-red-secondary"
aria-hidden="true"
/>
<div className="sr-only">Delete</div>
</button>
</div>
</div>
</Link>
<p className="pointer-events-none mt-2 block truncate text-sm font-medium text-blue-primary">
{session.name}
</p>
<p className="pointer-events-none block text-sm font-medium text-blue-primary">
{new Intl.DateTimeFormat(locale, {
dateStyle: "medium",
timeStyle: "medium",
}).format(session.lastModified)}
</p>
</div>
</div>
</li>
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/SessionsPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const SessionsPage = (p: SessionsPageProps): JSX.Element => (
account={p.account}
/>
</div>
<div className="mx-1 max-h-screen overflow-auto lg:mx-4">
<div className="mx-1 max-h-screen overflow-auto rounded-sm bg-grey-primary p-3 shadow-inner lg:mx-4">
<SessionList sessions={p.sessions} />
</div>
<div className="mx-1 lg:mx-4">
Expand Down

0 comments on commit 4b31801

Please sign in to comment.