diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts index 534dc198..25255266 100644 --- a/api/src/controllers/videos.ts +++ b/api/src/controllers/videos.ts @@ -204,7 +204,8 @@ const videoRelations = { .select({ pk: entries.showPk }) .from(entries) .innerJoin(entryVideoJoin, eq(entryVideoJoin.entryPk, entries.pk)) - .where(eq(videos.pk, entryVideoJoin.videoPk)), + .where(eq(videos.pk, entryVideoJoin.videoPk)) + .limit(1), ), ) .limit(1) diff --git a/front/bun.lock b/front/bun.lock index da85512a..a7eca93c 100644 --- a/front/bun.lock +++ b/front/bun.lock @@ -16,6 +16,7 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@react-navigation/bottom-tabs": "^7.4.0", + "@react-navigation/material-top-tabs": "^7.4.23", "@react-navigation/native": "^7.1.8", "@tanstack/react-query": "^5.91.3", "@tanstack/react-query-devtools": "^5.91.3", @@ -48,6 +49,7 @@ "react-native-localization-settings": "^1.2.0", "react-native-mmkv": "^4.2.0", "react-native-nitro-modules": "^0.35.2", + "react-native-pager-view": "^8.0.1", "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "~5.6.2", "react-native-screens": "~4.23.0", @@ -544,6 +546,8 @@ "@react-navigation/elements": ["@react-navigation/elements@2.9.11", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.34", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-O5KiwaVCcEVuqZgQ77xiBFSl1sha77rNMTFlLWYnom33ZHPDarV3bM9WNyVnMZxU8ZVTi02X3+ZhO0fSn5QYyg=="], + "@react-navigation/material-top-tabs": ["@react-navigation/material-top-tabs@7.4.23", "", { "dependencies": { "@react-navigation/elements": "^2.9.14", "color": "^4.2.3", "react-native-tab-view": "^4.3.0" }, "peerDependencies": { "@react-navigation/native": "^7.2.2", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0", "react-native-safe-area-context": ">= 4.0.0" } }, "sha512-yIU2ne7w+LBjhhiHLwhLIuzjnY44GK6OXqls1112StR4afd5bYYUBL6WopZkBmXD/3sRooimp8iy5Cih+8Mr8w=="], + "@react-navigation/native": ["@react-navigation/native@7.1.34", "", { "dependencies": { "@react-navigation/core": "^7.16.2", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-zzQ0mKAhLsjTIsaoLfILKZVMObJzE0F+bOi0hl2Glt+1Rd2GtaWJ1Z024c3yLmX+Oc79pqoCQLBXpyxtrZu9NQ=="], "@react-navigation/native-stack": ["@react-navigation/native-stack@7.14.6", "", { "dependencies": { "@react-navigation/elements": "^2.9.11", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.34", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-VRlC5mLanRPHK0E15Cild6U01Z5TDPBlmt5YcXRBc+hQTAMbMT9XcSTobf3sJXNY0zzDD1IpSs3Ynex/GU225g=="], @@ -1338,6 +1342,8 @@ "react-native-nitro-modules": ["react-native-nitro-modules@0.35.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-97cZcCh3ZAuWAfutel2Q3qLfc45XXh7F9Ei5tEjahP0kV3q8hQelwLIulKXmjN+f0JI5Zf/wCsfwwdVWYU2tKA=="], + "react-native-pager-view": ["react-native-pager-view@8.0.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-pGOne2o0y0HOQLrlTLcGgOE48uJlqSZHRRwdW8nL6JJozMkPGJYi/G9e0EsJoWFpXYONjiDgr8IwxC4F6/r7Lg=="], + "react-native-reanimated": ["react-native-reanimated@4.2.1", "", { "dependencies": { "react-native-is-edge-to-edge": "1.2.1", "semver": "7.7.3" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": ">=0.7.0" } }, "sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg=="], "react-native-safe-area-context": ["react-native-safe-area-context@5.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg=="], @@ -1348,6 +1354,8 @@ "react-native-svg-transformer": ["react-native-svg-transformer@1.5.3", "", { "dependencies": { "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "path-dirname": "^1.0.2" }, "peerDependencies": { "react-native": ">=0.59.0", "react-native-svg": ">=12.0.0" } }, "sha512-M4uFg5pUt35OMgjD4rWWbwd6PmxV96W7r/gQTTa+iZA5B+jO6aURhzAZGLHSrg1Kb91cKG0Rildy9q1WJvYstg=="], + "react-native-tab-view": ["react-native-tab-view@4.3.0", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-qPMF75uz/7+MuVG2g+YETdGMzlWZnhC6iI4h/7EBbwIBwNBIBi2z4OA6KhY3IOOBwGHXEIz5IyA6doDqifYBHg=="], + "react-native-video": ["react-native-video@github:zoriya/react-native-video#4bfa59b", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": ">=0.35.0" } }, "zoriya-react-native-video-4bfa59b", "sha512-rh3YPcQz+Uj/aMeDkSzFGOjgvvYvaizC85lywrY630KcrLg2vq+0vTwNTzARjjsdNOR3BCX4Ad/J3WEI5IHaxQ=="], "react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="], @@ -1650,6 +1658,8 @@ "@react-native/community-cli-plugin/metro-core": ["metro-core@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.5" } }, "sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ=="], + "@react-navigation/material-top-tabs/@react-navigation/elements": ["@react-navigation/elements@2.9.14", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.2.2", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-lKqzu+su2pI/YIZmR7L7xdOs4UL+rVXKJAMpRMBrwInEy96SjIFst6QDGpE89Dunnu3VjVpjWfByo9f2GWBHDQ=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], "@tailwindcss/node/tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], diff --git a/front/package.json b/front/package.json index 55d67ab0..aa8667ee 100644 --- a/front/package.json +++ b/front/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@react-navigation/bottom-tabs": "^7.4.0", + "@react-navigation/material-top-tabs": "^7.4.23", "@react-navigation/native": "^7.1.8", "@tanstack/react-query": "^5.91.3", "@tanstack/react-query-devtools": "^5.91.3", @@ -59,6 +60,7 @@ "react-native-localization-settings": "^1.2.0", "react-native-mmkv": "^4.2.0", "react-native-nitro-modules": "^0.35.2", + "react-native-pager-view": "^8.0.1", "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "~5.6.2", "react-native-screens": "~4.23.0", diff --git a/front/src/app/(app)/(tabs)/_layout.tsx b/front/src/app/(app)/(tabs)/_layout.tsx index eca02da5..e6f55cf6 100644 --- a/front/src/app/(app)/(tabs)/_layout.tsx +++ b/front/src/app/(app)/(tabs)/_layout.tsx @@ -1,12 +1,7 @@ -import Browse from "@material-symbols/svg-400/rounded/browse-fill.svg"; -// import Downloading from "@material-symbols/svg-400/rounded/downloading-fill.svg"; -import Home from "@material-symbols/svg-400/rounded/home-fill.svg"; -import Person from "@material-symbols/svg-400/rounded/person-fill.svg"; -import { Slot, Tabs } from "expo-router"; +import { Slot } from "expo-router"; +import { NativeTabs } from "expo-router/unstable-native-tabs"; import { useTranslation } from "react-i18next"; import { Platform } from "react-native"; -import { Icon } from "~/primitives"; -import { cn } from "~/utils"; export const unstable_settings = { initialRouteName: "index", @@ -18,49 +13,39 @@ export default function TabsLayout() { if (Platform.OS === "web") return ; return ( - - { - return ( - - ); - }, - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - + + + + {t("navbar.home")} + + + + + {t("navbar.browse")} + + + + + + {t("navbar.profile")} + + + + + {t("navbar.admin")} + + ); } diff --git a/front/src/app/(app)/(tabs)/admin/_layout.tsx b/front/src/app/(app)/(tabs)/admin/_layout.tsx new file mode 100644 index 00000000..19333aa1 --- /dev/null +++ b/front/src/app/(app)/(tabs)/admin/_layout.tsx @@ -0,0 +1,86 @@ +import { + createMaterialTopTabNavigator, + type MaterialTopTabNavigationEventMap, + type MaterialTopTabNavigationOptions, +} from "@react-navigation/material-top-tabs"; +import type { + NavigationProp, + ParamListBase, + TabNavigationState, +} from "@react-navigation/native"; +import { useFocusEffect, useNavigation } from "@react-navigation/native"; +import { Slot, withLayoutContext } from "expo-router"; +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; +import { useCSSVariable, useResolveClassNames } from "uniwind"; + +const { Navigator } = createMaterialTopTabNavigator(); + +const TopTabs = withLayoutContext< + MaterialTopTabNavigationOptions, + typeof Navigator, + TabNavigationState, + MaterialTopTabNavigationEventMap +>(Navigator); + +export const unstable_settings = { + initialRouteName: "unmatched", +}; + +export default function AdminTabsLayout() { + const { t } = useTranslation(); + const navigation = useNavigation>(); + const accent = useCSSVariable("--color-accent"); + const { color: activeColor } = useResolveClassNames("text-slate-100"); + const { color: inactiveColor } = useResolveClassNames("text-slate-400"); + const { color: borderColor } = useResolveClassNames("border-slate-700"); + + useFocusEffect( + useCallback(() => { + for (let nav = navigation; nav; nav = nav.getParent()) { + nav.setOptions({ headerShadowVisible: false }); + } + + return () => { + for (let nav = navigation; nav; nav = nav.getParent()) { + nav.setOptions({ headerShadowVisible: undefined }); + } + }; + }, [navigation]), + ); + + if (Platform.OS === "web") return ; + + return ( + + + + + ); +} diff --git a/front/src/app/(app)/unmatched.tsx b/front/src/app/(app)/(tabs)/admin/unmatched.tsx similarity index 100% rename from front/src/app/(app)/unmatched.tsx rename to front/src/app/(app)/(tabs)/admin/unmatched.tsx diff --git a/front/src/app/(app)/admin/users.tsx b/front/src/app/(app)/(tabs)/admin/users.tsx similarity index 100% rename from front/src/app/(app)/admin/users.tsx rename to front/src/app/(app)/(tabs)/admin/users.tsx diff --git a/front/src/ui/login/oidc.tsx b/front/src/ui/login/oidc.tsx index be720bbf..f274b959 100644 --- a/front/src/ui/login/oidc.tsx +++ b/front/src/ui/login/oidc.tsx @@ -55,5 +55,5 @@ export const OidcLogin = ({ apiUrl }: { apiUrl: string }) => { OidcLogin.query = (apiUrl?: string): QueryIdentifier => ({ path: ["auth", "info"], parser: AuthInfo, - options: { apiUrl }, + options: { apiUrl, returnError: true }, }); diff --git a/front/src/ui/navbar.tsx b/front/src/ui/navbar.tsx index 065625c5..53556f27 100644 --- a/front/src/ui/navbar.tsx +++ b/front/src/ui/navbar.tsx @@ -1,18 +1,27 @@ import Admin from "@material-symbols/svg-400/rounded/admin_panel_settings.svg"; import Register from "@material-symbols/svg-400/rounded/app_registration.svg"; +import Browse from "@material-symbols/svg-400/rounded/browse-fill.svg"; import Close from "@material-symbols/svg-400/rounded/close.svg"; import Login from "@material-symbols/svg-400/rounded/login.svg"; import Logout from "@material-symbols/svg-400/rounded/logout.svg"; +import Person from "@material-symbols/svg-400/rounded/person-fill.svg"; import Search from "@material-symbols/svg-400/rounded/search-fill.svg"; import Settings from "@material-symbols/svg-400/rounded/settings.svg"; import { useIsFocused } from "@react-navigation/native"; import { useNavigation, usePathname, useRouter } from "expo-router"; import KyooLongLogo from "public/icon-long.svg"; -import { type ComponentProps, useLayoutEffect, useRef, useState } from "react"; +import { + type ComponentProps, + type ComponentType, + useLayoutEffect, + useRef, + useState, +} from "react"; import { useTranslation } from "react-i18next"; import { Platform, type PressableProps, + Text, TextInput, type TextInputProps, View, @@ -30,7 +39,7 @@ import { A, Avatar, HR, - HRP, + Icon, IconButton, Menu, PressableFeedback, @@ -48,22 +57,53 @@ export const NavbarLeft = () => { return ( - - {t("navbar.browse")} - - + - {t("navbar.profile")} - + label={t("navbar.profile")} + icon={Person} + /> + + + + ); }; +const NavbarLink = >({ + as, + label, + icon, + ...props +}: { + as?: ComponentType; + label: string; + icon: ComponentProps["icon"]; +} & AsProps) => { + const As = as ?? A; + return ( + + + + {label} + + + ); +}; + export const NavbarTitle = ({ className, ...props @@ -260,17 +300,6 @@ export const NavbarProfile = () => { /> )} - {account?.isAdmin && ( - <> - - - - - )} ); };