Language dropdown, language in settings, add temporary confirmation to delete account

This commit is contained in:
Jip Fr 2023-11-18 20:55:46 +01:00
parent 54cd1d52ca
commit 2b23353e40
12 changed files with 160 additions and 79 deletions

View File

@ -4,5 +4,8 @@
"eslint.format.enable": true, "eslint.format.enable": true,
"[json]": { "[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
} }
} }

1
public/skull.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#CCD6DD" d="M27.865 16.751c0-6.242-4.411-9.988-9.927-9.988s-9.835 3.746-9.835 9.988c0 3.48-.103 6.485 3.897 7.89v2.722c0 1.034.966 1.872 2 1.872 1.035 0 2-.838 2-1.872v-1.97 1.97c0 1.034.965 1.872 2 1.872 1.036 0 2-.838 2-1.872v-1.97 1.97c0 1.034.966 1.872 2 1.872s2-.838 2-1.872v-2.722c4-1.405 3.865-4.41 3.865-7.89z"/><circle fill="#292F33" cx="13.629" cy="15.503" r="3.121"/><path fill="#292F33" d="M25.488 15.503c0 1.724 0 3.121-3.121 3.121-3.12 0-3.12-1.397-3.12-3.121s1.396-3.121 3.12-3.121c1.725 0 3.121 1.397 3.121 3.121zm-6.301 5.656c-.157-.382-.626-.662-1.189-.662-.561 0-1.031.28-1.188.662-.394.11-.685.469-.685.898 0 .517.419.936.937.936.409 0 .753-.263.88-.628.019 0 .037.004.056.004.019 0 .037-.004.057-.004.128.365.472.628.88.628.517 0 .936-.419.936-.936 0-.429-.291-.786-.684-.898z"/><path d="M11 27c0-.367.075-.713.195-1.038-.984-.447-1.831-1.082-2.503-1.97-1.107.969-2.163 1.876-3.127 2.695C4.985 26.26 4.275 26 3.5 26 1.567 26 0 27.566 0 29.5c0 1.778 1.33 3.229 3.046 3.454C3.271 34.671 4.722 36 6.5 36c1.933 0 3.5-1.566 3.5-3.5 0-.775-.26-1.485-.686-2.065.6-.706 1.246-1.46 1.931-2.25C11.088 27.821 11 27.421 11 27zm16.872-15.482c.884-.769 1.729-1.495 2.515-2.163.569.403 1.262.645 2.013.645 1.934 0 3.5-1.567 3.5-3.5 0-1.743-1.277-3.177-2.945-3.444C32.735 1.335 31.281 0 29.5 0 27.566 0 26 1.567 26 3.5c0 .775.26 1.485.687 2.065-.594.7-1.233 1.445-1.911 2.227 1.3.871 2.361 2.095 3.096 3.726zM3.5 10c.775 0 1.485-.26 2.065-.687.799.679 1.661 1.419 2.564 2.204.735-1.631 1.795-2.855 3.096-3.726-.679-.781-1.317-1.527-1.912-2.226.427-.58.687-1.29.687-2.065C10 1.567 8.433 0 6.5 0 4.722 0 3.271 1.33 3.046 3.046 1.33 3.271 0 4.722 0 6.5 0 8.433 1.567 10 3.5 10zm28.9 16c-.752 0-1.444.242-2.014.645-.952-.809-1.99-1.701-3.079-2.653-.672.889-1.519 1.523-2.503 1.971.121.324.196.67.196 1.037 0 .421-.088.821-.245 1.185.685.79 1.331 1.544 1.931 2.25-.426.58-.686 1.29-.686 2.065 0 1.934 1.566 3.5 3.5 3.5 1.781 0 3.235-1.334 3.455-3.056 1.668-.267 2.945-1.701 2.945-3.444 0-1.934-1.566-3.5-3.5-3.5z" fill="#AAB8C2"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -6,6 +6,7 @@ import { Icon, Icons } from "@/components/Icon";
export interface OptionItem { export interface OptionItem {
id: string; id: string;
name: string; name: string;
leftIcon?: React.ReactNode;
} }
interface DropdownProps { interface DropdownProps {
@ -20,12 +21,17 @@ export function Dropdown(props: DropdownProps) {
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}> <Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
{({ open }) => ( {({ open }) => (
<> <>
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-denim-500 py-2 pl-3 pr-10 text-left text-white shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-bink-500 focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-bink-300 sm:text-sm"> <Listbox.Button className="relative w-full cursor-default rounded-lg bg-dropdown-background py-3 pl-3 pr-10 text-left text-white shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-bink-500 focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-bink-300">
<span className="block truncate">{props.selectedItem.name}</span> <span className="flex gap-4 items-center truncate">
{props.selectedItem.leftIcon
? props.selectedItem.leftIcon
: null}
{props.selectedItem.name}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<Icon <Icon
icon={Icons.CHEVRON_DOWN} icon={Icons.CHEVRON_DOWN}
className={`transform transition-transform ${ className={`transform transition-transform text-xl ${
open ? "rotate-180" : "" open ? "rotate-180" : ""
}`} }`}
/> />
@ -37,17 +43,18 @@ export function Dropdown(props: DropdownProps) {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute left-0 right-0 top-10 z-10 mt-1 max-h-60 overflow-auto rounded-md bg-denim-500 py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 focus:outline-none sm:top-10 sm:text-sm"> <Listbox.Options className="absolute left-0 right-0 top-10 z-[1] mt-4 max-h-60 overflow-auto rounded-md bg-dropdown-background py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 focus:outline-none sm:top-10">
{props.options.map((opt) => ( {props.options.map((opt) => (
<Listbox.Option <Listbox.Option
className={({ active }) => className={({ active }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${ `flex gap-4 items-center relative cursor-default select-none py-3 pl-4 pr-4 ${
active ? "bg-denim-400 text-bink-700" : "text-white" active ? "bg-denim-400 text-bink-700" : "text-white"
}` }`
} }
key={opt.id} key={opt.id}
value={opt} value={opt}
> >
{opt.leftIcon ? opt.leftIcon : null}
{opt.name} {opt.name}
</Listbox.Option> </Listbox.Option>
))} ))}

View File

@ -6,11 +6,40 @@ export interface FlagIconProps {
} }
export function FlagIcon(props: FlagIconProps) { export function FlagIcon(props: FlagIconProps) {
// Country code overrides
const countryOverrides: Record<string, string> = {
en: "gb",
cs: "cz",
el: "gr",
fa: "ir",
ko: "kr",
he: "il",
ze: "cn",
ar: "sa",
ja: "jp",
bs: "ba",
vi: "vn",
zh: "cn",
sl: "si",
};
let countryCode =
(props.countryCode || "")?.split("-").pop()?.toLowerCase() || "";
if (countryOverrides[countryCode])
countryCode = countryOverrides[countryCode];
if (countryCode === "pirate")
return (
<div className="w-8 h-6 rounded bg-[#2E3439] flex justify-center items-center">
<img src="/skull.svg" className="w-4 h-4" />
</div>
);
return ( return (
<span <span
className={classNames( className={classNames(
"!w-8 h-6 rounded overflow-hidden bg-video-context-flagBg bg-cover bg-center block fi", "!w-8 h-6 rounded overflow-hidden bg-video-context-flagBg bg-cover bg-center block fi",
props.countryCode ? `fi-${props.countryCode}` : undefined props.countryCode ? `fi-${countryCode}` : undefined
)} )}
/> />
); );

View File

@ -13,6 +13,7 @@ import { getLanguageFromIETF } from "@/components/player/utils/language";
import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
import { useSubtitleStore } from "@/stores/subtitles"; import { useSubtitleStore } from "@/stores/subtitles";
import { sortLangCodes } from "@/utils/sortLangCodes";
export function CaptionOption(props: { export function CaptionOption(props: {
countryCode?: string; countryCode?: string;
@ -22,24 +23,6 @@ export function CaptionOption(props: {
onClick?: () => void; onClick?: () => void;
error?: React.ReactNode; error?: React.ReactNode;
}) { }) {
// Country code overrides
const countryOverrides: Record<string, string> = {
en: "gb",
cs: "cz",
el: "gr",
fa: "ir",
ko: "kr",
he: "il",
ze: "cn",
ar: "sa",
ja: "jp",
bs: "ba",
};
let countryCode =
(props.countryCode || "")?.split("-").pop()?.toLowerCase() || "";
if (countryOverrides[countryCode])
countryCode = countryOverrides[countryCode];
return ( return (
<SelectableLink <SelectableLink
selected={props.selected} selected={props.selected}
@ -52,7 +35,7 @@ export function CaptionOption(props: {
className="flex items-center" className="flex items-center"
> >
<span data-code={props.countryCode} className="mr-3"> <span data-code={props.countryCode} className="mr-3">
<FlagIcon countryCode={countryCode} /> <FlagIcon countryCode={props.countryCode} />
</span> </span>
<span>{props.children}</span> <span>{props.children}</span>
</span> </span>
@ -64,19 +47,12 @@ function searchSubs(
subs: (SubtitleSearchItem & { languageName: string })[], subs: (SubtitleSearchItem & { languageName: string })[],
searchQuery: string searchQuery: string
) { ) {
const languagesOrder = ["en", "hi", "fr", "de", "nl", "pt"].reverse(); // Reverse is neccesary, not sure why const sorted = sortLangCodes(subs.map((t) => t.attributes.language));
let results = subs.sort((a, b) => { let results = subs.sort((a, b) => {
if ( return (
languagesOrder.indexOf(b.attributes.language) !== -1 || sorted.indexOf(a.attributes.language) -
languagesOrder.indexOf(a.attributes.language) !== -1 sorted.indexOf(b.attributes.language)
) );
return (
languagesOrder.indexOf(b.attributes.language) -
languagesOrder.indexOf(a.attributes.language)
);
return a.languageName.localeCompare(b.languageName);
}); });
if (searchQuery.trim().length > 0) { if (searchQuery.trim().length > 0) {
@ -152,7 +128,7 @@ export function CaptionsView({ id }: { id: string }) {
if (req.loading) content = <p>loading...</p>; if (req.loading) content = <p>loading...</p>;
else if (req.error) content = <p>errored!</p>; else if (req.error) content = <p>errored!</p>;
else if (req.value) { else if (req.value) {
const subs = req.value.map((v) => { const subs = req.value.filter(Boolean).map((v) => {
const languageName = const languageName =
getLanguageFromIETF(v.attributes.language) ?? "unknown"; getLanguageFromIETF(v.attributes.language) ?? "unknown";
return { return {

View File

@ -18,6 +18,7 @@ import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { useThemeStore } from "@/stores/theme"; import { useThemeStore } from "@/stores/theme";
import { SubPageLayout } from "./layouts/SubPageLayout"; import { SubPageLayout } from "./layouts/SubPageLayout";
import { LocalePart } from "./settings/LocalePart";
function SettingsLayout(props: { children: React.ReactNode }) { function SettingsLayout(props: { children: React.ReactNode }) {
const { isMobile } = useIsMobile(); const { isMobile } = useIsMobile();
@ -79,6 +80,9 @@ export function SettingsPage() {
<RegisterCalloutPart /> <RegisterCalloutPart />
)} )}
</div> </div>
<div id="settings-locale">
<LocalePart />
</div>
<div id="settings-appearance"> <div id="settings-appearance">
<ThemePart active={activeTheme} setTheme={setTheme} /> <ThemePart active={activeTheme} setTheme={setTheme} />
</div> </div>

View File

@ -14,6 +14,8 @@ export function AccountActionsPart() {
const { logout } = useAuthData(); const { logout } = useAuthData();
const [deleteResult, deleteExec] = useAsyncFn(async () => { const [deleteResult, deleteExec] = useAsyncFn(async () => {
if (!account) return; if (!account) return;
// eslint-disable-next-line no-restricted-globals
if (!confirm("You sure bro?")) return;
await deleteUser(url, account); await deleteUser(url, account);
logout(); logout();
}, [logout, account, url]); }, [logout, account, url]);

View File

@ -0,0 +1,36 @@
import { Dropdown } from "@/components/Dropdown";
import { FlagIcon } from "@/components/FlagIcon";
import { Heading1 } from "@/components/utils/Text";
import { appLanguageOptions } from "@/setup/i18n";
import { useLanguageStore } from "@/stores/language";
import { sortLangCodes } from "@/utils/sortLangCodes";
export function LocalePart() {
const sorted = sortLangCodes(appLanguageOptions.map((t) => t.id));
const { language, setLanguage } = useLanguageStore();
const options = appLanguageOptions
.sort((a, b) => sorted.indexOf(a.id) - sorted.indexOf(b.id))
.map((opt) => ({
id: opt.id,
name: `${opt.englishName}${opt.nativeName}`,
leftIcon: <FlagIcon countryCode={opt.id} />,
}));
const selected = options.find((t) => t.id === language);
return (
<div>
<Heading1 border>Locale</Heading1>
<p className="text-white font-bold mb-3">Application language</p>
<p className="max-w-[20rem] font-medium">
Language applied to the entire application.
</p>
<Dropdown
options={options}
selectedItem={selected || options[0]}
setSelectedItem={(opt) => setLanguage(opt.id)}
/>
</div>
);
}

View File

@ -18,6 +18,7 @@ export function SidebarPart() {
const settingLinks = [ const settingLinks = [
{ text: "Account", id: "settings-account", icon: Icons.USER }, { text: "Account", id: "settings-account", icon: Icons.USER },
{ text: "Locale", id: "settings-locale", icon: Icons.LINK },
{ text: "Appearance", id: "settings-appearance", icon: Icons.GITHUB }, { text: "Appearance", id: "settings-appearance", icon: Icons.GITHUB },
{ text: "Captions", id: "settings-captions", icon: Icons.CAPTIONS }, { text: "Captions", id: "settings-captions", icon: Icons.CAPTIONS },
]; ];
@ -35,10 +36,10 @@ export function SidebarPart() {
const visible = !( const visible = !(
Math.floor( Math.floor(
100 - ((rect.top >= 0 ? 0 : rect.top) / +-rect.height) * 100 50 - ((rect.top >= 0 ? 0 : rect.top) / +-rect.height) * 100
) < percentageVisible || ) < percentageVisible ||
Math.floor( Math.floor(
100 - ((rect.bottom - windowHeight) / rect.height) * 100 50 - ((rect.bottom - windowHeight) / rect.height) * 100
) < percentageVisible ) < percentageVisible
); );
@ -80,6 +81,7 @@ export function SidebarPart() {
icon={v.icon} icon={v.icon}
active={v.id === activeLink} active={v.id === activeLink}
onClick={() => scrollTo(v.id)} onClick={() => scrollTo(v.id)}
key={v.id}
> >
{v.text} {v.text}
</SidebarLink> </SidebarLink>

View File

@ -1,5 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer"; import { immer } from "zustand/middleware/immer";
import i18n from "@/setup/i18n"; import i18n from "@/setup/i18n";
@ -10,14 +11,17 @@ export interface LanguageStore {
} }
export const useLanguageStore = create( export const useLanguageStore = create(
immer<LanguageStore>((set) => ({ persist(
language: "en", immer<LanguageStore>((set) => ({
setLanguage(v) { language: "en",
set((s) => { setLanguage(v) {
s.language = v; set((s) => {
}); s.language = v;
}, });
})) },
})),
{ name: "__MW::locale" }
)
); );
export function useLanguageListener() { export function useLanguageListener() {

View File

@ -0,0 +1,12 @@
export function sortLangCodes(langCodes: string[]) {
const languagesOrder = ["en", "hi", "fr", "de", "nl", "pt"].reverse(); // Reverse is neccesary, not sure why
const results = langCodes.sort((a, b) => {
if (languagesOrder.indexOf(b) !== -1 || languagesOrder.indexOf(a) !== -1)
return languagesOrder.indexOf(b) - languagesOrder.indexOf(a);
return a.localeCompare(b);
});
return results;
}

View File

@ -4,23 +4,23 @@ export const defaultTheme = {
themePreview: { themePreview: {
primary: "#505DBD", primary: "#505DBD",
secondary: "#73739D", secondary: "#73739D",
ghost: "white" ghost: "white",
}, },
// Branding // Branding
pill: { pill: {
background: "#1C1C36" background: "#1C1C36",
}, },
// meta data for the theme itself // meta data for the theme itself
global: { global: {
accentA: "#505DBD", accentA: "#505DBD",
accentB: "#3440A1" accentB: "#3440A1",
}, },
// light bar // light bar
lightBar: { lightBar: {
light: "#2A2A71" light: "#2A2A71",
}, },
// Buttons // Buttons
@ -39,14 +39,14 @@ export const defaultTheme = {
purple: "#6b298a", purple: "#6b298a",
purpleHover: "#7f35a1", purpleHover: "#7f35a1",
cancel: "#252533", cancel: "#252533",
cancelHover: "#3C3C4A" cancelHover: "#3C3C4A",
}, },
// only used for body colors/textures // only used for body colors/textures
background: { background: {
main: "#0A0A10", main: "#0A0A10",
accentA: "#6E3B80", accentA: "#6E3B80",
accentB: "#1F1F50" accentB: "#1F1F50",
}, },
// typography // typography
@ -55,7 +55,7 @@ export const defaultTheme = {
text: "#73739D", text: "#73739D",
dimmed: "#926CAD", dimmed: "#926CAD",
divider: "#262632", divider: "#262632",
secondary: "#64647B" secondary: "#64647B",
}, },
// search bar // search bar
@ -64,7 +64,7 @@ export const defaultTheme = {
focused: "#24243C", focused: "#24243C",
placeholder: "#4A4A71", placeholder: "#4A4A71",
icon: "#545476", icon: "#545476",
text: "#FFFFFF" text: "#FFFFFF",
}, },
// media cards // media cards
@ -76,13 +76,18 @@ export const defaultTheme = {
barColor: "#4B4B63", barColor: "#4B4B63",
barFillColor: "#BA7FD6", barFillColor: "#BA7FD6",
badge: "#151522", badge: "#151522",
badgeText: "#5F5F7A" badgeText: "#5F5F7A",
}, },
// Large card // Large card
largeCard: { largeCard: {
background: "#171728", background: "#171728",
icon: "#6741A5" icon: "#6741A5",
},
// Dropdown
dropdown: {
background: "#171728",
}, },
// Passphrase // Passphrase
@ -92,7 +97,7 @@ export const defaultTheme = {
wordBackground: "#171728", wordBackground: "#171728",
copyText: "#58587A", copyText: "#58587A",
copyTextHover: "#8888AA", copyTextHover: "#8888AA",
errorText: "#DB3D62" errorText: "#DB3D62",
}, },
// Settings page // Settings page
@ -105,19 +110,19 @@ export const defaultTheme = {
inactive: "#8D68A9", inactive: "#8D68A9",
icon: "#926CAD", icon: "#926CAD",
iconActivated: "#6942A8", iconActivated: "#6942A8",
activated: "#CBA1E8" activated: "#CBA1E8",
} },
}, },
card: { card: {
border: "#2A243E", border: "#2A243E",
background: "#29243D", background: "#29243D",
altBackground: "#29243D" altBackground: "#29243D",
} },
}, },
utils: { utils: {
divider: "#353549" divider: "#353549",
}, },
// Error page // Error page
@ -126,20 +131,20 @@ export const defaultTheme = {
border: "#252534", border: "#252534",
type: { type: {
secondary: "#62627D" secondary: "#62627D",
} },
}, },
// About page // About page
about: { about: {
circle: "#262632", circle: "#262632",
circleText: "#9A9AC3" circleText: "#9A9AC3",
}, },
progress: { progress: {
background: "#8787A8", background: "#8787A8",
preloaded: "#8787A8", preloaded: "#8787A8",
filled: "#A75FC9" filled: "#A75FC9",
}, },
// video player // video player
@ -151,11 +156,11 @@ export const defaultTheme = {
error: "#E44F4F", error: "#E44F4F",
success: "#40B44B", success: "#40B44B",
loading: "#B759D8", loading: "#B759D8",
noresult: "#64647B" noresult: "#64647B",
}, },
audio: { audio: {
set: "#A75FC9" set: "#A75FC9",
}, },
context: { context: {
@ -175,16 +180,16 @@ export const defaultTheme = {
buttons: { buttons: {
list: "#161C26", list: "#161C26",
active: "#0D1317" active: "#0D1317",
}, },
type: { type: {
main: "#617A8A", main: "#617A8A",
secondary: "#374A56", secondary: "#374A56",
accent: "#A570FA" accent: "#A570FA",
} },
} },
} },
} },
} },
} };