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,
"[json]": {
"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 {
id: string;
name: string;
leftIcon?: React.ReactNode;
}
interface DropdownProps {
@ -20,12 +21,17 @@ export function Dropdown(props: DropdownProps) {
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
{({ 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">
<span className="block truncate">{props.selectedItem.name}</span>
<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="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">
<Icon
icon={Icons.CHEVRON_DOWN}
className={`transform transition-transform ${
className={`transform transition-transform text-xl ${
open ? "rotate-180" : ""
}`}
/>
@ -37,17 +43,18 @@ export function Dropdown(props: DropdownProps) {
leaveFrom="opacity-100"
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) => (
<Listbox.Option
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"
}`
}
key={opt.id}
value={opt}
>
{opt.leftIcon ? opt.leftIcon : null}
{opt.name}
</Listbox.Option>
))}

View File

@ -6,11 +6,40 @@ export interface 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 (
<span
className={classNames(
"!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 { usePlayerStore } from "@/stores/player/store";
import { useSubtitleStore } from "@/stores/subtitles";
import { sortLangCodes } from "@/utils/sortLangCodes";
export function CaptionOption(props: {
countryCode?: string;
@ -22,24 +23,6 @@ export function CaptionOption(props: {
onClick?: () => void;
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 (
<SelectableLink
selected={props.selected}
@ -52,7 +35,7 @@ export function CaptionOption(props: {
className="flex items-center"
>
<span data-code={props.countryCode} className="mr-3">
<FlagIcon countryCode={countryCode} />
<FlagIcon countryCode={props.countryCode} />
</span>
<span>{props.children}</span>
</span>
@ -64,19 +47,12 @@ function searchSubs(
subs: (SubtitleSearchItem & { languageName: 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) => {
if (
languagesOrder.indexOf(b.attributes.language) !== -1 ||
languagesOrder.indexOf(a.attributes.language) !== -1
)
return (
languagesOrder.indexOf(b.attributes.language) -
languagesOrder.indexOf(a.attributes.language)
);
return a.languageName.localeCompare(b.languageName);
return (
sorted.indexOf(a.attributes.language) -
sorted.indexOf(b.attributes.language)
);
});
if (searchQuery.trim().length > 0) {
@ -152,7 +128,7 @@ export function CaptionsView({ id }: { id: string }) {
if (req.loading) content = <p>loading...</p>;
else if (req.error) content = <p>errored!</p>;
else if (req.value) {
const subs = req.value.map((v) => {
const subs = req.value.filter(Boolean).map((v) => {
const languageName =
getLanguageFromIETF(v.attributes.language) ?? "unknown";
return {

View File

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

View File

@ -14,6 +14,8 @@ export function AccountActionsPart() {
const { logout } = useAuthData();
const [deleteResult, deleteExec] = useAsyncFn(async () => {
if (!account) return;
// eslint-disable-next-line no-restricted-globals
if (!confirm("You sure bro?")) return;
await deleteUser(url, account);
logout();
}, [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 = [
{ 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: "Captions", id: "settings-captions", icon: Icons.CAPTIONS },
];
@ -35,10 +36,10 @@ export function SidebarPart() {
const visible = !(
Math.floor(
100 - ((rect.top >= 0 ? 0 : rect.top) / +-rect.height) * 100
50 - ((rect.top >= 0 ? 0 : rect.top) / +-rect.height) * 100
) < percentageVisible ||
Math.floor(
100 - ((rect.bottom - windowHeight) / rect.height) * 100
50 - ((rect.bottom - windowHeight) / rect.height) * 100
) < percentageVisible
);
@ -80,6 +81,7 @@ export function SidebarPart() {
icon={v.icon}
active={v.id === activeLink}
onClick={() => scrollTo(v.id)}
key={v.id}
>
{v.text}
</SidebarLink>

View File

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