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 && (
- <>
-
-
-
- >
- )}
);
};