Skip to content

Commit

Permalink
support orgs with images + playlist entry
Browse files Browse the repository at this point in the history
  • Loading branch information
sphinxrave committed Jul 21, 2024
1 parent 277fce8 commit 79b411e
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 44 deletions.
2 changes: 1 addition & 1 deletion packages/react/src/components/layout/InlayContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function InlayContainer({ routes }: InlayContainerProps) {
{t("component.mainNav.back")}
</Button>
</div>
<div className="w-full rounded-lg bg-base-2 p-2 md:p-4">
<div className="w-full rounded-lg bg-base-2 p-2 md:p-4 xl:p-8">
<Suspense fallback={<Loading size="xl" />}>
<Outlet />
</Suspense>
Expand Down
23 changes: 15 additions & 8 deletions packages/react/src/components/org/OrgPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ import {
} from "@/shadcn/ui/command";
import { Popover, PopoverTrigger, PopoverContent } from "@/shadcn/ui/popover";
import { useTranslation } from "react-i18next";
import { currentOrgAtom } from "@/store/org";
import { useAtom } from "jotai/react";
import { getThumbnailForOrg } from "@/lib/thumb";

export function OrgSelectorCombobox() {
export function OrgSelectorCombobox({
org,
setOrg,
}: {
org?: Org;
setOrg?: (org: Org) => void;
}) {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState("");
const [currentOrg, setCurrentOrg] = useAtom(currentOrgAtom);
// const [currentOrg, setOrg] = useAtom(orgAtom)
const [value, setValue] = React.useState(org?.name || "");

// Use the useOrgs API service to fetch organizations
const { data: orgs, isError } = useOrgs();
const { data: orgs, isError } = useOrgs({ enabled: open });

if (isError) {
return <div>Error fetching organizations</div>;
Expand Down Expand Up @@ -63,8 +66,8 @@ export function OrgSelectorCombobox() {
key={org.name}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue);
setCurrentOrg(org);
setOpen(false);
setOrg?.(org);
}}
>
<div
Expand All @@ -73,6 +76,10 @@ export function OrgSelectorCombobox() {
value === org.name ? "opacity-100" : "opacity-0",
)}
/>
<img
className="mr-2 h-8 w-8 rounded-full"
src={getThumbnailForOrg(org.icon)}
></img>
{org.name}
</CommandItem>
))}
Expand Down
93 changes: 65 additions & 28 deletions packages/react/src/components/playlist/PlaylistEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { makeYtThumbnailUrl } from "@/lib/utils";
import { TypographyLarge, TypographyP } from "@/shadcn/ui/typography";
import React from "react";
import { Link } from "react-router-dom";
import { useAtomValue } from "jotai";
import { localeAtom } from "@/store/i18n";
import { useTranslation } from "react-i18next";
import { buttonVariants } from "@/shadcn/ui/button.variants";
import { Link } from "react-router-dom";
import { Button } from "@/shadcn/ui/button";
import { makeYtThumbnailUrl } from "@/lib/utils";
import { localeAtom } from "@/store/i18n";
import { userAtom } from "@/store/auth";
import DeletePlaylistDialog from "@/components/playlist/DeletePlaylistDialog";
import { Button } from "@/shadcn/ui/button";
import { VideoThumbnail } from "../video/VideoThumbnail";
import DeletePlaylistDialog from "@/components/playlist/DeletePlaylistDialog";

export default function PlaylistEntry({
video_ids,
Expand All @@ -22,39 +21,77 @@ export default function PlaylistEntry({
const user = useAtomValue(userAtom);

return (
<div className="mt-5 flex gap-5">
<VideoThumbnail
className="h-28"
src={makeYtThumbnailUrl(video_ids[0], "sm")}
/>
<div>
<TypographyLarge>{name}</TypographyLarge>
<TypographyP className="!mt-0">
<div className="flex items-center gap-4 rounded-lg bg-base-2 p-4 shadow-sm">
<div className="shrink-0">
{video_ids && video_ids.length > 0 ? (
<VideoThumbnail
className="aspect-video h-24 rounded-md object-cover"
src={makeYtThumbnailUrl(video_ids[0], "sm")}
/>
) : (
<div className="flex aspect-video h-24 items-center justify-center rounded-md bg-base-5 p-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="4em"
height="4em"
viewBox="0 0 36 36"
>
<path
fill="currentColor"
d="m34.59 23l-4.08-5l4-4.9a1.82 1.82 0 0 0 .23-1.94a1.93 1.93 0 0 0-1.8-1.16h-31A1.91 1.91 0 0 0 0 11.88v12.25A1.91 1.91 0 0 0 1.94 26h31.11a1.93 1.93 0 0 0 1.77-1.09a1.82 1.82 0 0 0-.23-1.91M2 24V12h30.78l-4.84 5.93L32.85 24Z"
className="clr-i-outline clr-i-outline-path-1"
></path>
<path
fill="currentColor"
d="M9.39 19.35L6.13 15H5v6.18h1.13v-4.34l3.26 4.34h1.12V15H9.39z"
className="clr-i-outline clr-i-outline-path-2"
></path>
<path
fill="currentColor"
d="M12.18 21.18h4.66v-1.02h-3.53v-1.61h3.19v-1.03h-3.19v-1.49h3.53V15h-4.66z"
className="clr-i-outline clr-i-outline-path-3"
></path>
<path
fill="currentColor"
d="M24.52 19.43L23.06 15h-1.22l-1.47 4.43L19.05 15h-1.23l1.96 6.18h1.11l1.56-4.59L24 21.18h1.13L27.08 15h-1.23z"
className="clr-i-outline clr-i-outline-path-4"
></path>
<path fill="none" d="M0 0h36v36H0z"></path>
</svg>
</div>
)}
</div>
<div className="grow">
<h3 className="mb-1 text-lg font-semibold">{name}</h3>
<span className="text-sm text-base-10">
{video_ids?.length || 0} {t("views.channel.video")}
</span>
<span className="ml-4 text-sm text-base-10">
{t("views.playlist.item-last-updated") +
" " +
dayjs(updated_at).format("LLL")}
</TypographyP>
<div className="mt-5 flex gap-3">
<Button size="icon" variant="secondary">
</span>
<div className="mt-2 flex gap-2">
<Button size="sm" variant="primary" className="w-20">
<span className="i-heroicons:play-solid" />
</Button>
<Link
to={`/playlist/${id}`}
className={buttonVariants({ size: "icon", variant: "ghost" })}
>
<span className="i-heroicons:pencil-square-solid" />
</Link>
{user?.id === user_id ? (
<Button size="sm" variant="outline" className="w-20" asChild>
<Link to={`/playlist/${id}`}>
<span className="i-heroicons:pencil-square-solid mr-1" />
{t("component.videoCard.edit")}
</Link>
</Button>
{user?.id === user_id && (
<DeletePlaylistDialog
triggerElement={
<Button size="icon" variant="ghost">
<span className="i-heroicons:trash-solid" />
<Button size="sm" variant="ghost">
<span className="i-heroicons:trash-solid mr-1" />
</Button>
}
playlistId={id}
playlistName={name}
/>
) : null}
)}
</div>
</div>
</div>
Expand Down
43 changes: 43 additions & 0 deletions packages/react/src/lib/thumb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getChannelPhoto } from "./utils";

/**
* Generates a thumbnail URL for a given image URL and resolution definition.
* If the provided URL does not start with "http", the original URL is returned.
* Otherwise, the URL is encoded and formatted according to the specified definition.
*
* @param imgUrl The original image URL.
* @param definition The desired resolution of the thumbnail. Can be "maxres" or "default".
* @returns A string representing the formatted thumbnail URL or the original URL.
*/
export function getThumbnailUrl(
imgUrl: string,
definition: "maxres" | "default",
) {
// Check if imgUrl starts with "http", indicating it's an external URL
if (imgUrl.startsWith("http") === false) {
return imgUrl; // Return the original URL if it's not an external URL
}
// Encode the URL and replace characters to make it URL-safe
const encodedUrl = btoa(imgUrl)
.replace("+", "-")
.replace("/", "_")
.replace(/=+$/, ""); // Remove any trailing '=' characters

return `/statics/thumbnail/${definition}/${encodedUrl}.jpg`;
}

type MaybeUndefined<T extends string> = T | undefined;
type NullIfUndefined<T extends string> = T extends undefined ? null : T;

export function getThumbnailForOrg<T extends string>(
orgIcon: MaybeUndefined<T>,
): NullIfUndefined<T> {
if (!orgIcon) return null as NullIfUndefined<T>;

if (orgIcon.startsWith("UC")) {
// it's a channel icon:
return getChannelPhoto(orgIcon, 20) as NullIfUndefined<T>;
}

return getThumbnailUrl(orgIcon, "default") as NullIfUndefined<T>;
}
19 changes: 14 additions & 5 deletions packages/react/src/routes/playlists.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React, { useState } from "react";
import { usePlaylists } from "@/services/playlist.service";
import {
usePlaylistSaveMutation,
usePlaylists,
} from "@/services/playlist.service";
import PlaylistEntry from "@/components/playlist/PlaylistEntry";
import { TypographyH3, TypographyP } from "@/shadcn/ui/typography";
import { useTranslation } from "react-i18next";
Expand All @@ -17,21 +20,27 @@ import {
} from "@/shadcn/ui/dialog";
import { Input } from "@/shadcn/ui/input";
import { useNavigate } from "react-router-dom";
import { useClient } from "@/hooks/useClient";

export function Playlists() {
const { data: playlists, refetch } = usePlaylists();
const { mutateAsync: savePlaylist } = usePlaylistSaveMutation();
const { t } = useTranslation();
const user = useAtomValue(userAtom);
const fetchClient = useClient();
const [isNewPlaylistDialogOpen, setIsNewPlaylistDialogOpen] = useState(false);
const [newPlaylistName, setNewPlaylistName] = useState("");
const navigate = useNavigate();

const handleCreateNewPlaylist = async () => {
// TODO: Implement the API call to create a new playlist
console.log("Creating new playlist:", newPlaylistName);
setIsNewPlaylistDialogOpen(false);
setNewPlaylistName("");
await refetch();
const newId = await savePlaylist({
name: newPlaylistName,
video_ids: [],
user_id: user?.id,
});
};

return (
Expand All @@ -49,7 +58,7 @@ export function Playlists() {
}
>
<div className="flex items-center p-4">
<span className="i-heroicons:playlist-plus mr-3 text-4xl" />
<div className="i-lucide:list-plus mr-3 text-xl" />
<div>
<TypographyP className="font-medium">
{t("views.playlist.new-playlist-btn-label")}
Expand All @@ -72,7 +81,7 @@ export function Playlists() {
open={isNewPlaylistDialogOpen}
onOpenChange={setIsNewPlaylistDialogOpen}
>
<DialogContent>
<DialogContent aria-description="Create new playlist">
<DialogHeader>
<DialogTitle>
{t("views.playlist.new-playlist-btn-label")}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/routes/settings/lang.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function SettingsLang() {
<SettingsItem label={t("views.settings.clipLanguageSelection")} fullWidth>
<div className="grid gap-3">
{TL_LANGS.map(({ text, value }) => (
<div className="flex items-center gap-3">
<div className="flex items-center gap-3" key={"cliplang-" + value}>
<Checkbox
id={`cliplang-${value}`}
checked={clipLangs.includes(value)}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/services/orgs.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export function useOrgs(config?: CommonQueryConfig) {
return useQuery<Org[], Error>({
queryKey: ["orgs"],
queryFn: async () =>
fetch(`${window.location.origin}/statics/orgs.json`).then((r) => {
fetch(`${window.location.origin}/statics/orgsV2.json`).then((r) => {
if (!r.ok) {
throw new Error(`HTTP error! Status: ${r.status}`);
}
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/types/org.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ interface Org {
name: string;
short?: string;
name_jp?: string;
icon?: string;
}

0 comments on commit 79b411e

Please sign in to comment.