Refactor popup and modal to display the same thing

This commit is contained in:
Zoe Roux 2026-03-12 10:21:04 +01:00
parent 64b3ad437c
commit f4be92f190
No known key found for this signature in database
5 changed files with 135 additions and 180 deletions

View File

@ -1,15 +1,16 @@
import { H1, P, Popup } from "~/primitives";
import { P, Popup } from "~/primitives";
export const EntrySelect = ({
name,
videos,
close,
}: {
name: string;
videos: { slug: string; path: string }[];
close?: () => void;
}) => {
return (
<Popup>
<H1>{name}</H1>
<Popup title={name} close={close}>
{videos.map((x) => (
<P key={x.slug}>{x.path}</P>
))}

View File

@ -1,16 +1,15 @@
import Close from "@material-symbols/svg-400/rounded/close.svg";
import { Stack, useRouter } from "expo-router";
import type { ReactNode } from "react";
import { Pressable, ScrollView, View } from "react-native";
import { cn } from "~/utils";
import { IconButton } from "./icons";
import { Heading } from "./text";
import type { Icon } from "./icons";
import { Overlay } from "./popup";
export const Modal = ({
icon,
title,
children,
scroll = true,
}: {
icon?: Icon;
title: string;
children: ReactNode;
scroll?: boolean;
@ -31,28 +30,9 @@ export const Modal = ({
},
}}
/>
<Pressable
className="absolute inset-0 cursor-default! items-center justify-center bg-black/60 max-md:px-4"
onPress={close}
>
<Pressable
className={cn(
"w-full max-w-3xl rounded-md bg-background",
"max-h-[90vh] cursor-default! overflow-hidden",
)}
onPress={(e) => e.preventDefault()}
>
<View className="flex-row items-center justify-between p-6">
<Heading>{title}</Heading>
<IconButton icon={Close} onPress={close} />
</View>
{scroll ? (
<ScrollView className="p-6">{children}</ScrollView>
) : (
<View className="flex-1">{children}</View>
)}
</Pressable>
</Pressable>
<Overlay icon={icon} title={title} close={close} scroll={scroll}>
{children}
</Overlay>
</>
);
};

View File

@ -1,65 +1,70 @@
import { usePortal } from "@gorhom/portal";
import Close from "@material-symbols/svg-400/rounded/close.svg";
import { type ReactNode, useCallback, useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import { px, vh } from "yoshiki/native";
import { Container } from "./container";
import { ContrastArea, SwitchVariant, type YoshikiFunc } from "./theme";
import { ts } from "./utils";
import { Pressable, ScrollView, View } from "react-native";
import { cn } from "~/utils";
import { Icon, IconButton, type Icon as IconType } from "./icons";
import { Heading } from "./text";
export const Popup = ({
export const Overlay = ({
icon,
title,
close,
children,
...props
scroll = true,
}: {
children: ReactNode | YoshikiFunc<ReactNode>;
icon?: IconType;
title: string;
close?: () => void;
children: ReactNode;
scroll?: boolean;
}) => {
return (
<ContrastArea mode="user">
<SwitchVariant>
{({ css, theme }) => (
<View
{...css({
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
bg: (theme) => theme.themeOverlay,
justifyContent: "center",
alignItems: "center",
})}
>
<Container
{...css(
{
borderRadius: px(6),
paddingHorizontal: 0,
bg: (theme) => theme.background,
overflow: "hidden",
},
props,
)}
>
<ScrollView
contentContainerStyle={{
paddingHorizontal: px(15),
paddingVertical: ts(4),
gap: ts(2),
}}
{...css({
maxHeight: vh(95),
flexGrow: 0,
flexShrink: 1,
})}
>
{typeof children === "function"
? children({ css, theme })
: children}
</ScrollView>
</Container>
</View>
<Pressable
className="absolute inset-0 cursor-default! items-center justify-center bg-black/60 max-md:px-4"
onPress={close}
>
<Pressable
className={cn(
"w-full max-w-3xl rounded-md bg-background",
"max-h-[90vh] cursor-default! overflow-hidden",
)}
</SwitchVariant>
</ContrastArea>
onPress={(e) => e.preventDefault()}
>
<View className="flex-row items-center justify-between p-6">
<View className="flex-row items-center gap-2">
{icon && <Icon icon={icon} />}
<Heading>{title}</Heading>
</View>
{close && <IconButton icon={Close} onPress={close} />}
</View>
{scroll ? (
<ScrollView className="p-6">{children}</ScrollView>
) : (
<View className="flex-1">{children}</View>
)}
</Pressable>
</Pressable>
);
};
export const Popup = ({
icon,
title,
close,
children,
scroll,
}: {
icon?: IconType;
title: string;
close?: () => void;
children: ReactNode;
scroll?: boolean;
}) => {
return (
<Overlay icon={icon} title={title} close={close} scroll={scroll}>
{children}
</Overlay>
);
};

View File

@ -108,16 +108,22 @@ export const SerieDetails = () => {
const { scrollHandler, headerProps, headerHeight } = useScrollNavbar({
imageHeight,
});
const [setPopup] = usePopup();
const [setPopup, closePopup] = usePopup();
const openEntrySelect = useCallback(
(entry: {
name: string | null;
videos: { slug: string; path: string }[];
}) => {
setPopup(<EntrySelect name={entry.name!} videos={entry.videos} />);
setPopup(
<EntrySelect
name={entry.name ?? ""}
videos={entry.videos}
close={closePopup}
/>,
);
},
[setPopup],
[setPopup, closePopup],
);
return (

View File

@ -9,17 +9,14 @@ import { type ComponentProps, useState } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useUniwind } from "uniwind";
import { rem } from "yoshiki/native";
import type { KyooError, User } from "~/models";
import {
Alert,
Button,
H1,
Icon,
type Icon,
Input,
P,
Popup,
ts,
usePopup,
} from "~/primitives";
import { useAccount } from "~/providers/account-context";
@ -226,43 +223,27 @@ const ChangePopup = ({
const [value, setValue] = useState(inital);
return (
<Popup>
{({ css }) => (
<>
<View
{...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}
>
<Icon icon={icon} />
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
</View>
<Input
autoComplete={autoComplete}
value={value}
onChangeText={(v) => setValue(v)}
/>
<View
{...css({
flexDirection: "row",
alignSelf: "flex-end",
gap: ts(1),
})}
>
<Button
text={t("misc.cancel")}
onPress={() => close()}
{...css({ minWidth: rem(6) })}
/>
<Button
text={t("misc.edit")}
onPress={async () => {
await apply(value);
close();
}}
{...css({ minWidth: rem(6) })}
/>
</View>
</>
)}
<Popup title={label} icon={icon} close={close}>
<Input
autoComplete={autoComplete}
value={value}
onChangeText={(v) => setValue(v)}
/>
<View className="flex-row gap-2 self-end">
<Button
text={t("misc.cancel")}
onPress={() => close()}
className="min-w-24"
/>
<Button
text={t("misc.edit")}
onPress={async () => {
await apply(value);
close();
}}
className="min-w-24"
/>
</View>
</Popup>
);
};
@ -286,59 +267,41 @@ const ChangePasswordPopup = ({
const [error, setError] = useState<string | null>(null);
return (
<Popup>
{({ css }) => (
<>
<View
{...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}
>
<Icon icon={icon} />
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
</View>
{hasPassword && (
<PasswordInput
autoComplete="current-password"
value={oldValue}
onChangeText={(v) => setOldValue(v)}
placeholder={t("settings.account.password.oldPassword")}
/>
)}
<PasswordInput
autoComplete="new-password"
value={newValue}
onChangeText={(v) => setNewValue(v)}
placeholder={t("settings.account.password.newPassword")}
/>
{error && (
<P {...css({ color: (theme) => theme.colors.red })}>{error}</P>
)}
<View
{...css({
flexDirection: "row",
alignSelf: "flex-end",
gap: ts(1),
})}
>
<Button
text={t("misc.cancel")}
onPress={() => close()}
{...css({ minWidth: rem(6) })}
/>
<Button
text={t("misc.edit")}
onPress={async () => {
try {
await apply(oldValue, newValue);
close();
} catch (e) {
setError((e as KyooError).message);
}
}}
{...css({ minWidth: rem(6) })}
/>
</View>
</>
<Popup title={label} icon={icon} close={close}>
{hasPassword && (
<PasswordInput
autoComplete="current-password"
value={oldValue}
onChangeText={(v) => setOldValue(v)}
placeholder={t("settings.account.password.oldPassword")}
/>
)}
<PasswordInput
autoComplete="new-password"
value={newValue}
onChangeText={(v) => setNewValue(v)}
placeholder={t("settings.account.password.newPassword")}
/>
{error && <P className="text-red-500">{error}</P>}
<View className="flex-row gap-2 self-end">
<Button
text={t("misc.cancel")}
onPress={() => close()}
className="min-w-24"
/>
<Button
text={t("misc.edit")}
onPress={async () => {
try {
await apply(oldValue, newValue);
close();
} catch (e) {
setError((e as KyooError).message);
}
}}
className="min-w-24"
/>
</View>
</Popup>
);
};