}>
diff --git a/packages/react/src/components/org/OrgPicker.tsx b/packages/react/src/components/org/OrgPicker.tsx
index 1180a6f79..9ebde89b1 100644
--- a/packages/react/src/components/org/OrgPicker.tsx
+++ b/packages/react/src/components/org/OrgPicker.tsx
@@ -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
Error fetching organizations
;
@@ -63,8 +66,8 @@ export function OrgSelectorCombobox() {
key={org.name}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue);
- setCurrentOrg(org);
setOpen(false);
+ setOrg?.(org);
}}
>
+
{org.name}
))}
diff --git a/packages/react/src/components/playlist/PlaylistEntry.tsx b/packages/react/src/components/playlist/PlaylistEntry.tsx
index 98a3ed51e..5eaf06817 100644
--- a/packages/react/src/components/playlist/PlaylistEntry.tsx
+++ b/packages/react/src/components/playlist/PlaylistEntry.tsx
@@ -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,
@@ -22,39 +21,77 @@ export default function PlaylistEntry({
const user = useAtomValue(userAtom);
return (
-
-
-
-
{name}
-
+
+
+ {video_ids && video_ids.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+
{name}
+
+ {video_ids?.length || 0} {t("views.channel.video")}
+
+
{t("views.playlist.item-last-updated") +
" " +
dayjs(updated_at).format("LLL")}
-
-
-
diff --git a/packages/react/src/lib/thumb.ts b/packages/react/src/lib/thumb.ts
new file mode 100644
index 000000000..02c006e8e
--- /dev/null
+++ b/packages/react/src/lib/thumb.ts
@@ -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 | undefined;
+type NullIfUndefined = T extends undefined ? null : T;
+
+export function getThumbnailForOrg(
+ orgIcon: MaybeUndefined,
+): NullIfUndefined {
+ if (!orgIcon) return null as NullIfUndefined;
+
+ if (orgIcon.startsWith("UC")) {
+ // it's a channel icon:
+ return getChannelPhoto(orgIcon, 20) as NullIfUndefined;
+ }
+
+ return getThumbnailUrl(orgIcon, "default") as NullIfUndefined;
+}
diff --git a/packages/react/src/routes/playlists.tsx b/packages/react/src/routes/playlists.tsx
index f3050b759..a5043d9d0 100644
--- a/packages/react/src/routes/playlists.tsx
+++ b/packages/react/src/routes/playlists.tsx
@@ -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";
@@ -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 (
@@ -49,7 +58,7 @@ export function Playlists() {
}
>
-
+
{t("views.playlist.new-playlist-btn-label")}
@@ -72,7 +81,7 @@ export function Playlists() {
open={isNewPlaylistDialogOpen}
onOpenChange={setIsNewPlaylistDialogOpen}
>
-
+
{t("views.playlist.new-playlist-btn-label")}
diff --git a/packages/react/src/routes/settings/lang.tsx b/packages/react/src/routes/settings/lang.tsx
index 886f8c4d4..4ee3555e8 100644
--- a/packages/react/src/routes/settings/lang.tsx
+++ b/packages/react/src/routes/settings/lang.tsx
@@ -106,7 +106,7 @@ export function SettingsLang() {
{TL_LANGS.map(({ text, value }) => (
-
+
({
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}`);
}
diff --git a/packages/react/src/types/org.d.ts b/packages/react/src/types/org.d.ts
index 31d375556..fd7d156fb 100644
--- a/packages/react/src/types/org.d.ts
+++ b/packages/react/src/types/org.d.ts
@@ -2,4 +2,5 @@ interface Org {
name: string;
short?: string;
name_jp?: string;
+ icon?: string;
}