-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
441 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { Button, Placeholder, Stack } from "@quotepedia/solid"; | ||
import { useTranslator } from "~/lib/i18n"; | ||
|
||
export const ErrorBoundaryFallback = (err: any, reset: () => void) => { | ||
const t = useTranslator(); | ||
|
||
return ( | ||
<Placeholder | ||
heading={"An error occured:"} | ||
description={ | ||
<code class="bg-bg-default ring-bg-secondary select-all rounded-lg p-1.5 ring-1">{err?.toString()}</code> | ||
} | ||
footer={ | ||
<Stack.Horizontal> | ||
<Button | ||
as={"a"} | ||
rel="noopener noreferrer" | ||
href={import.meta.env.APP_BUGS_URL} | ||
target="_blank" | ||
color="secondary" | ||
spacing="lg" | ||
leadingIcon="f7:flag" | ||
> | ||
{t("settings.about.feedback.heading")} | ||
</Button> | ||
<Button onClick={reset} spacing="lg" leadingIcon="f7:arrow-clockwise"> | ||
{t("continue")} | ||
</Button> | ||
</Stack.Horizontal> | ||
} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { A } from "@solidjs/router"; | ||
import { splitProps, type ComponentProps } from "solid-js"; | ||
import { tv } from "tailwind-variants"; | ||
import type { Collection } from "~/lib/api/collections"; | ||
import { usePlurarized } from "~/lib/i18n"; | ||
import EmojiImg from "../Emoji"; | ||
|
||
export const styles = tv({ | ||
slots: { | ||
root: [ | ||
"group relative flex divide-x divide-bg-secondary rounded-xl !shadow-sm", | ||
"ring-1 ring-bg-secondary transition-all duration-300", | ||
"before:absolute before:inset-0 before:-z-10 before:-translate-y-1 before:scale-95 before:rounded-xl", | ||
"before:bg-bg-default before:ring-1 before:ring-bg-secondary before:transition-transform before:duration-300", | ||
"hover:divide-bg-tertiary hover:ring-bg-tertiary", | ||
"before:hover:-translate-y-1 hover:before:scale-95 hover:before:ring-bg-tertiary active:opacity-50", | ||
], | ||
emoji: "ease-spring size-6 transition-transform duration-300 group-hover:scale-125", | ||
thumb: "bg-bg-default flex items-center rounded-s-xl p-3", | ||
hgroup: "bg-bg-body w-full grow space-y-1.5 rounded-e-xl p-4 ps-3", | ||
h3: "text-nowrap font-medium leading-none", | ||
p: "text-fg-muted text-nowrap text-sm leading-none", | ||
}, | ||
variants: { | ||
active: { | ||
true: { | ||
root: [ | ||
"divide-bg-tertiary opacity-75 outline outline-4", | ||
"outline-ring-accent ring-bg-tertiary before:scale-95 before:ring-bg-tertiary", | ||
], | ||
emoji: "scale-125", | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
export type CollectionCardOptions = { | ||
collection: Collection; | ||
active?: boolean; | ||
class?: string; | ||
}; | ||
|
||
export type CollectionCardProps = CollectionCardOptions & Omit<ComponentProps<typeof A>, "href">; | ||
|
||
export const CollectionCard = (props: CollectionCardProps) => { | ||
const [variantProps, localProps, otherProps] = splitProps(props, ["active"], ["collection", "class"]); | ||
return ( | ||
<A | ||
href={`/library/collections/${props.collection.id}`} | ||
class={styles().root({ ...variantProps, class: localProps.class })} | ||
{...otherProps} | ||
> | ||
<span class={styles().thumb(variantProps)}> | ||
<EmojiImg emoji={localProps.collection.emote} class={styles().emoji(variantProps)} /> | ||
</span> | ||
<hgroup class={styles().hgroup(variantProps)}> | ||
<h3 class={styles().h3(variantProps)}>{localProps.collection.name}</h3> | ||
<p class={styles().p(variantProps)}>{usePlurarized("quotes", localProps.collection.quotes_count)}</p> | ||
</hgroup> | ||
</A> | ||
); | ||
}; |
25 changes: 25 additions & 0 deletions
25
apps/web/src/components/collections/CollectionCardSkeleton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { cn, Skeleton } from "@quotepedia/solid"; | ||
import { splitProps, type ComponentProps } from "solid-js"; | ||
|
||
export const CollectionCardSkeleton = (props: ComponentProps<"div">) => { | ||
const [localProps, otherProps] = splitProps(props, ["class"]); | ||
return ( | ||
<div | ||
class={cn( | ||
"divide-bg-secondary ring-bg-secondary relative flex min-w-56 divide-x rounded-xl ring-1", | ||
"before:bg-bg-default before:ring-bg-secondary before:absolute before:inset-0", | ||
"before:-z-10 before:-translate-y-1 before:scale-95 before:rounded-xl before:ring-1", | ||
localProps.class, | ||
)} | ||
{...otherProps} | ||
> | ||
<div class="bg-bg-default flex items-center rounded-s-xl p-3"> | ||
<Skeleton opacity={75} class="size-6 rounded-full" /> | ||
</div> | ||
<div class="bg-bg-body w-full grow space-y-1.5 overflow-hidden rounded-e-xl p-4 ps-3"> | ||
<Skeleton opacity={75} class="h-4 rounded-md" /> | ||
<Skeleton opacity={50} class="h-3.5 w-9/12 rounded-md" /> | ||
</div> | ||
</div> | ||
); | ||
}; |
13 changes: 13 additions & 0 deletions
13
apps/web/src/components/collections/CollectionSidebarItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import type { components } from "~/lib/api"; | ||
import { Sidebar } from "../aside/sidebar"; | ||
import EmojiImg from "../Emoji"; | ||
|
||
export const CollectionSidebarItem = (props: { collection: components["schemas"]["CollectionResponse"] }) => { | ||
return ( | ||
<Sidebar.Item href={`/library/collections/${props.collection.id}`} class="group"> | ||
<Sidebar.ItemIcon as={EmojiImg} emoji={props.collection.emote} /> | ||
<Sidebar.ItemLabel>{props.collection.name}</Sidebar.ItemLabel> | ||
<span class="text-fg-muted ms-auto text-base leading-none">{props.collection.quotes_count}</span> | ||
</Sidebar.Item> | ||
); | ||
}; |
20 changes: 20 additions & 0 deletions
20
apps/web/src/components/collections/CollectionSidebarItemList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { For } from "solid-js"; | ||
import { TransitionGroup } from "solid-transition-group"; | ||
import type { components } from "~/lib/api"; | ||
import { CollectionSidebarItem } from "./CollectionSidebarItem"; | ||
|
||
export const CollectionSidebarItemList = (props: { collections: components["schemas"]["CollectionResponse"][] }) => { | ||
return ( | ||
<div class="relative flex grow flex-col space-y-1 overflow-y-scroll"> | ||
<TransitionGroup | ||
moveClass="!transition-all !duration-500" | ||
enterActiveClass="!transition-all !duration-500" | ||
exitActiveClass="!transition-all !duration-500 !absolute" | ||
enterClass="!opacity-0" | ||
exitToClass="!opacity-0" | ||
> | ||
<For each={props.collections}>{(collection) => <CollectionSidebarItem collection={collection} />}</For> | ||
</TransitionGroup> | ||
</div> | ||
); | ||
}; |
126 changes: 126 additions & 0 deletions
126
apps/web/src/components/collections/CollectionsInfinitePaginator.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { Lottie, Placeholder, Search } from "@quotepedia/solid"; | ||
import { Repeat } from "@solid-primitives/range"; | ||
import { useNavigate } from "@solidjs/router"; | ||
import { batch, createSelector, createSignal, Match, Show, Switch, type JSX } from "solid-js"; | ||
import { createList } from "solid-list"; | ||
import { WindowVirtualizer, type WindowVirtualizerHandle } from "virtua/solid"; | ||
import { CollectionCard, CollectionCardSkeleton } from "~/components/collections"; | ||
import { getCurrentUserCollections } from "~/lib/api/collections"; | ||
import { useTranslator } from "~/lib/i18n"; | ||
import { createInfinitePaginator } from "~/utils/pagination"; | ||
import { useSearchQuery } from "~/utils/router"; | ||
|
||
const SEARCH_WAIT = 300; | ||
const SCROLL_TRIGGER_THRESHOLD = 1.0; | ||
const SCROLL_TRIGGER_ROOT_MARGIN = "25%"; | ||
const COLLECTIONS_VIRTUALIZER_OVERSCAN = 5; | ||
const COLLECTIONS_PER_PAGE = 20; | ||
|
||
export const CollectionsInfinitePaginator = () => { | ||
const t = useTranslator(); | ||
const navigate = useNavigate(); | ||
const [searchQuery, setSearchQuery] = useSearchQuery(); | ||
|
||
const [virtualizer, setVirtualizer] = createSignal<WindowVirtualizerHandle>(); | ||
|
||
const [collections, setTriggerRef, paginator] = createInfinitePaginator( | ||
(page) => | ||
getCurrentUserCollections({ | ||
q: searchQuery(), | ||
limit: COLLECTIONS_PER_PAGE, | ||
offset: COLLECTIONS_PER_PAGE * page, | ||
}), | ||
{ | ||
threshold: SCROLL_TRIGGER_THRESHOLD, | ||
rootMargin: SCROLL_TRIGGER_ROOT_MARGIN, | ||
}, | ||
); | ||
|
||
const { active, setActive, onKeyDown } = createList({ | ||
loop: false, | ||
items: collections, | ||
onActiveChange: (collection) => { | ||
if (collection) { | ||
const index = collections().indexOf(collection); | ||
virtualizer()?.scrollToIndex(index, { align: "center" }); | ||
} | ||
}, | ||
}); | ||
|
||
const isActive = createSelector(active); | ||
|
||
const onSearchBlur: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, FocusEvent> = () => { | ||
setActive(null); | ||
}; | ||
|
||
const onSearchFocus: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, FocusEvent> = () => { | ||
setActive(null); | ||
}; | ||
|
||
const onSearchChange = (value: string) => { | ||
batch(() => { | ||
setActive(null); | ||
setSearchQuery(value); | ||
paginator.reset(); | ||
}); | ||
}; | ||
|
||
const onSearchKeyDown: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, KeyboardEvent> = (event) => { | ||
const collection = active(); | ||
|
||
if (event.key === "Enter" && collection !== null) { | ||
return navigate(`/library/collections/${collection.id}`); | ||
} | ||
|
||
onKeyDown(event); | ||
}; | ||
|
||
return ( | ||
<> | ||
<Search | ||
value={searchQuery()} | ||
onBlur={onSearchBlur} | ||
onFocus={onSearchFocus} | ||
onChange={onSearchChange} | ||
onKeyDown={onSearchKeyDown} | ||
placeholder={t("search")} | ||
wait={SEARCH_WAIT} | ||
autofocus | ||
/> | ||
|
||
<Switch> | ||
<Match when={collections().length > 0}> | ||
<WindowVirtualizer ref={setVirtualizer} data={collections()} overscan={COLLECTIONS_VIRTUALIZER_OVERSCAN}> | ||
{(collection) => <CollectionCard class="mt-4" collection={collection} active={isActive(collection)} />} | ||
</WindowVirtualizer> | ||
|
||
<Show when={!paginator.end()}> | ||
<div ref={setTriggerRef} /> | ||
</Show> | ||
</Match> | ||
|
||
<Match when={paginator.loading() === true && collections().length <= 0}> | ||
<Repeat times={COLLECTIONS_PER_PAGE}> | ||
<CollectionCardSkeleton class="mt-4" /> | ||
</Repeat> | ||
</Match> | ||
|
||
<Match when={paginator.loading() === false && collections().length <= 0 && searchQuery() !== ""}> | ||
<Placeholder | ||
icon={<Lottie path="/tgs/search.json" class="size-24" />} | ||
heading={t("noResults")} | ||
description={t("noResultsFor", { value: searchQuery() })} | ||
/> | ||
</Match> | ||
|
||
<Match when={paginator.loading() === false && collections().length <= 0 && searchQuery() === ""}> | ||
<Placeholder | ||
icon={<Lottie path="/tgs/spiderweb.json" class="size-24" />} | ||
heading={t("empty")} | ||
description={t("noCollections")} | ||
/> | ||
</Match> | ||
</Switch> | ||
</> | ||
); | ||
}; |
Oops, something went wrong.