Merge branch 'dev' into about-page
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
IconPlug,
|
||||
} from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { removeTrailingSlash } from 'next/dist/shared/lib/router/utils/remove-trailing-slash';
|
||||
import { useState } from 'react';
|
||||
import { useConfigContext } from '~/config/provider';
|
||||
import { useConfigStore } from '~/config/store';
|
||||
@@ -90,6 +91,8 @@ export const EditAppModal = ({
|
||||
return;
|
||||
}
|
||||
|
||||
values.url = removeTrailingSlash(values.url);
|
||||
|
||||
updateConfig(
|
||||
configName,
|
||||
(previousConfig) => ({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Stack, Tabs, Text, TextInput } from '@mantine/core';
|
||||
import { Anchor, Button, Card, Collapse, Group, Stack, Tabs, Text, TextInput } from '@mantine/core';
|
||||
import { UseFormReturnType } from '@mantine/form';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconClick, IconCursorText, IconLink } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { AppType } from '~/types/app';
|
||||
@@ -13,6 +14,19 @@ interface GeneralTabProps {
|
||||
|
||||
export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
||||
const { t } = useTranslation('layout/modals/add-app');
|
||||
|
||||
const [opened, { toggle }] = useDisclosure(false);
|
||||
|
||||
const commonMistakes = [
|
||||
t('general.internalAddress.troubleshoot.lines.nothingAfterPort'),
|
||||
t('general.internalAddress.troubleshoot.lines.protocolCheck'),
|
||||
t('general.internalAddress.troubleshoot.lines.preferIP'),
|
||||
t('general.internalAddress.troubleshoot.lines.enablePings'),
|
||||
t('general.internalAddress.troubleshoot.lines.wget'),
|
||||
t('general.internalAddress.troubleshoot.lines.iframe'),
|
||||
t('general.internalAddress.troubleshoot.lines.clearCache'),
|
||||
];
|
||||
|
||||
return (
|
||||
<Tabs.Panel value="general" pt="sm">
|
||||
<Stack spacing="xs">
|
||||
@@ -46,6 +60,27 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
||||
{...form.getInputProps('behaviour.externalUrl')}
|
||||
/>
|
||||
|
||||
<Collapse in={opened}>
|
||||
<Card withBorder>
|
||||
<Text>{t('general.internalAddress.troubleshoot.header')}</Text>
|
||||
{commonMistakes.map((value: string, key: number) => {
|
||||
return (
|
||||
<Group key={key} display="flex" style={{ alignItems: 'start' }}>
|
||||
<Text>•</Text>
|
||||
<Text style={{ flex: '1' }}>{value}</Text>
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
<Text>
|
||||
{t('general.internalAddress.troubleshoot.footer').split('{{discord}}')[0]}
|
||||
<Anchor href="https://discord.gg/aCsmEV5RgA" target="_blank">
|
||||
Discord
|
||||
</Anchor>
|
||||
{t('general.internalAddress.troubleshoot.footer').split('{{discord}}')[1]}
|
||||
</Text>
|
||||
</Card>
|
||||
</Collapse>
|
||||
|
||||
{!form.values.behaviour.externalUrl.startsWith('https://') &&
|
||||
!form.values.behaviour.externalUrl.startsWith('http://') && (
|
||||
<Text color="red" mt="sm" size="sm">
|
||||
@@ -53,6 +88,10 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Button onClick={toggle} bottom={-68} left={0} color="yellow.7" variant="light">
|
||||
{t('general.internalAddress.troubleshoot.label')}
|
||||
</Button>
|
||||
</Tabs.Panel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ export const CommonHead = () => {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<link rel="shortcut icon" href="/imgs/favicon/favicon.svg" />
|
||||
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link crossOrigin="use-credentials" rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
{/* configure apple splash screen & touch icon */}
|
||||
<link rel="apple-touch-icon" href="/imgs/favicon/favicon.svg" />
|
||||
|
||||
@@ -26,18 +26,17 @@ import { MainLayout } from './MainLayout';
|
||||
import { env } from 'process';
|
||||
|
||||
type BoardLayoutProps = {
|
||||
dockerEnabled: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const BoardLayout = ({ children, dockerEnabled }: BoardLayoutProps) => {
|
||||
export const BoardLayout = ({ children }: BoardLayoutProps) => {
|
||||
const { config } = useConfigContext();
|
||||
const { data: session } = useSession();
|
||||
|
||||
return (
|
||||
<MainLayout
|
||||
autoFocusSearch={session?.user.autoFocusSearch}
|
||||
headerActions={<HeaderActions dockerEnabled={dockerEnabled} />}
|
||||
headerActions={<HeaderActions />}
|
||||
>
|
||||
<BoardHeadOverride />
|
||||
<BackgroundImage />
|
||||
@@ -47,36 +46,19 @@ export const BoardLayout = ({ children, dockerEnabled }: BoardLayoutProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
type HeaderActionProps = {
|
||||
dockerEnabled: boolean;
|
||||
};
|
||||
|
||||
export const HeaderActions = ({ dockerEnabled }: HeaderActionProps) => {
|
||||
export const HeaderActions = () => {
|
||||
const { data: sessionData } = useSession();
|
||||
|
||||
if (!sessionData?.user?.isAdmin) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{dockerEnabled && <DockerButton />}
|
||||
<ToggleEditModeButton />
|
||||
<CustomizeBoardButton />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DockerButton = () => {
|
||||
const { t } = useTranslation('modules/docker');
|
||||
|
||||
return (
|
||||
<Tooltip label={t('actionIcon.tooltip')}>
|
||||
<HeaderActionButton component={Link} href="/manage/tools/docker">
|
||||
<IconBrandDocker size={20} stroke={1.5} />
|
||||
</HeaderActionButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomizeBoardButton = () => {
|
||||
const { name } = useConfigContext();
|
||||
const { t } = useTranslation('boards/common');
|
||||
|
||||
1
src/images/undraw_secure_login_pdn4.svg
Normal file
1
src/images/undraw_secure_login_pdn4.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.4 KiB |
55
src/pages/401.tsx
Normal file
55
src/pages/401.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Button, Center, Stack, Title, Text, createStyles } from "@mantine/core";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import Image from "next/image";
|
||||
import Head from "next/head";
|
||||
import { MainLayout } from "~/components/layout/Templates/MainLayout";
|
||||
import Link from "next/link";
|
||||
|
||||
import imageAccessDenied from '~/images/undraw_secure_login_pdn4.svg';
|
||||
import { pageAccessDeniedNamespaces } from "~/tools/server/translation-namespaces";
|
||||
import { getServerSideTranslations } from "~/tools/server/getServerSideTranslations";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
|
||||
export default function Custom401() {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation('layout/errors/access-denied');
|
||||
return (
|
||||
<MainLayout>
|
||||
<Center h="100dvh" w="100dvw">
|
||||
<Head>
|
||||
<title>Access denied • Homarr</title>
|
||||
</Head>
|
||||
<Stack maw={500} p="xl">
|
||||
<Image className={classes.image} src={imageAccessDenied} width={200} height={200} alt="" />
|
||||
<Title>{t('title')}</Title>
|
||||
<Text>{t('text')}</Text>
|
||||
|
||||
<Button component={Link} variant="light" href="/auth/login">
|
||||
{t('switchAccount')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps({ req, res, locale }: GetServerSidePropsContext) {
|
||||
const translations = await getServerSideTranslations(
|
||||
[...pageAccessDeniedNamespaces, 'common'],
|
||||
locale,
|
||||
req,
|
||||
res
|
||||
);
|
||||
return {
|
||||
props: {
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
image: {
|
||||
margin: '0 auto',
|
||||
display: 'block',
|
||||
},
|
||||
}));
|
||||
@@ -61,7 +61,7 @@ function App(
|
||||
const analyticsEnabled = pageProps.analyticsEnabled ?? true;
|
||||
// TODO: make mapping from our locales to moment locales
|
||||
const language = getLanguageByCode(pageProps.session?.user?.language ?? 'en');
|
||||
require(`dayjs/locale/${language.locale}.js`);
|
||||
if (language.locale !== 'cr') require(`dayjs/locale/${language.locale}.js`);
|
||||
dayjs.locale(language.locale);
|
||||
|
||||
const [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>(
|
||||
@@ -101,12 +101,18 @@ function App(
|
||||
return (
|
||||
<>
|
||||
<CommonHead />
|
||||
{pageProps.session && pageProps.session.user.language === 'cr' && (
|
||||
<>
|
||||
<Script type="text/javascript" src="//cdn.crowdin.com/jipt/jipt.js" />
|
||||
<Script type="text/javascript">var _jipt = []; _jipt.push(['project', 'homarr']);</Script>
|
||||
</>
|
||||
)}
|
||||
{analyticsEnabled === true && (
|
||||
<Script
|
||||
src="https://umami.homarr.dev/script.js"
|
||||
data-website-id="f133f10c-30a7-4506-889c-3a803f328fa4"
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
src="https://umami.homarr.dev/script.js"
|
||||
data-website-id="f133f10c-30a7-4506-889c-3a803f328fa4"
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
)}
|
||||
<SessionProvider session={pageProps.session}>
|
||||
<ColorSchemeProvider {...pageProps}>
|
||||
|
||||
@@ -15,12 +15,11 @@ import { ConfigType } from '~/types/config';
|
||||
|
||||
export default function BoardPage({
|
||||
config: initialConfig,
|
||||
dockerEnabled,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
useInitConfig(initialConfig);
|
||||
|
||||
return (
|
||||
<BoardLayout dockerEnabled={dockerEnabled}>
|
||||
<BoardLayout>
|
||||
<Dashboard />
|
||||
</BoardLayout>
|
||||
);
|
||||
@@ -28,7 +27,6 @@ export default function BoardPage({
|
||||
|
||||
type BoardGetServerSideProps = {
|
||||
config: ConfigType;
|
||||
dockerEnabled: boolean;
|
||||
_nextI18Next?: SSRConfig['_nextI18Next'];
|
||||
};
|
||||
|
||||
@@ -64,7 +62,7 @@ export const getServerSideProps: GetServerSideProps<BoardGetServerSideProps> = a
|
||||
const result = checkForSessionOrAskForLogin(
|
||||
ctx,
|
||||
session,
|
||||
() => config.settings.access.allowGuests || !!session?.user
|
||||
() => config.settings.access.allowGuests || session?.user != undefined
|
||||
);
|
||||
if (result) {
|
||||
return result;
|
||||
@@ -76,7 +74,6 @@ export const getServerSideProps: GetServerSideProps<BoardGetServerSideProps> = a
|
||||
primaryColor: config.settings.customization.colors.primary,
|
||||
secondaryColor: config.settings.customization.colors.secondary,
|
||||
primaryShade: config.settings.customization.colors.shade,
|
||||
dockerEnabled: !!env.DOCKER_HOST && !!env.DOCKER_PORT,
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@ import { MainLayout } from '~/components/layout/Templates/MainLayout';
|
||||
import { createTrpcServersideHelpers } from '~/server/api/helper';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
import { firstUpperCase } from '~/tools/shared/strings';
|
||||
import { api } from '~/utils/api';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
@@ -275,22 +276,22 @@ const routeParamsSchema = z.object({
|
||||
slug: z.string(),
|
||||
});
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res, locale, params }) => {
|
||||
const routeParams = routeParamsSchema.safeParse(params);
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const routeParams = routeParamsSchema.safeParse(context.params);
|
||||
if (!routeParams.success) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
if (!session?.user.isAdmin) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
const session = await getServerAuthSession({ req: context.req, res: context.res });
|
||||
|
||||
const result = checkForSessionOrAskForLogin(context, session, () => session?.user.isAdmin == true);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const helpers = await createTrpcServersideHelpers({ req, res });
|
||||
const helpers = await createTrpcServersideHelpers({ req: context.req, res: context.res });
|
||||
|
||||
const config = await helpers.config.byName.fetch({ name: routeParams.data.slug });
|
||||
|
||||
@@ -305,9 +306,9 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res, locale,
|
||||
'settings/customization/gridstack',
|
||||
'settings/customization/access',
|
||||
],
|
||||
locale,
|
||||
req,
|
||||
res
|
||||
context.locale,
|
||||
context.req,
|
||||
context.res
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import { SSRConfig } from 'next-i18next';
|
||||
import { Dashboard } from '~/components/Dashboard/Dashboard';
|
||||
import { BoardLayout } from '~/components/layout/Templates/BoardLayout';
|
||||
import { useInitConfig } from '~/config/init';
|
||||
import { env } from '~/env';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { db } from '~/server/db';
|
||||
import { getDefaultBoardAsync } from '~/server/db/queries/userSettings';
|
||||
import { userSettings } from '~/server/db/schema';
|
||||
import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
import { boardNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { ConfigType } from '~/types/config';
|
||||
|
||||
export default function BoardPage({
|
||||
config: initialConfig,
|
||||
dockerEnabled,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
useInitConfig(initialConfig);
|
||||
|
||||
return (
|
||||
<BoardLayout dockerEnabled={dockerEnabled}>
|
||||
<BoardLayout>
|
||||
<Dashboard />
|
||||
</BoardLayout>
|
||||
);
|
||||
@@ -45,15 +41,9 @@ export const getServerSideProps: GetServerSideProps<BoardGetServerSideProps> = a
|
||||
);
|
||||
const config = await getFrontendConfig(boardName);
|
||||
|
||||
if (!config?.settings?.access?.allowGuests && !session?.user) {
|
||||
return {
|
||||
notFound: true,
|
||||
props: {
|
||||
primaryColor: config.settings.customization.colors.primary,
|
||||
secondaryColor: config.settings.customization.colors.secondary,
|
||||
primaryShade: config.settings.customization.colors.shade,
|
||||
},
|
||||
};
|
||||
const result = checkForSessionOrAskForLogin(ctx, session, () => true);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -62,7 +52,6 @@ export const getServerSideProps: GetServerSideProps<BoardGetServerSideProps> = a
|
||||
primaryColor: config.settings.customization.colors.primary,
|
||||
secondaryColor: config.settings.customization.colors.secondary,
|
||||
primaryShade: config.settings.customization.colors.shade,
|
||||
dockerEnabled: !!env.DOCKER_HOST && !!env.DOCKER_PORT,
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import { getServerSideTranslations } from '~/tools/server/getServerSideTranslati
|
||||
import { OnlyKeysWithStructure } from '~/types/helpers';
|
||||
|
||||
import { type quickActions } from '../../../public/locales/en/manage/index.json';
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
|
||||
const ManagementPage = () => {
|
||||
const { t } = useTranslation('manage/index');
|
||||
@@ -118,10 +119,9 @@ const QuickActionCard = ({ type, href }: QuickActionCardProps) => {
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
if (!session?.user) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
const result = checkForSessionOrAskForLogin(ctx, session, () => true);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { dockerRouter } from '~/server/api/routers/docker/router';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
import { boardNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
@@ -54,24 +55,23 @@ export default function DockerPage({
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ locale, req, res }) => {
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
if (!session?.user.isAdmin) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const session = await getServerAuthSession({ req: context.req, res: context.res });
|
||||
const result = checkForSessionOrAskForLogin(context, session, () => session?.user.isAdmin == true);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const caller = dockerRouter.createCaller({
|
||||
session: session,
|
||||
cookies: req.cookies,
|
||||
cookies: context.req.cookies,
|
||||
});
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
[...boardNamespaces, 'layout/manage', 'tools/docker'],
|
||||
locale,
|
||||
req,
|
||||
res
|
||||
context.locale,
|
||||
context.req,
|
||||
context.res
|
||||
);
|
||||
|
||||
let containers = [];
|
||||
|
||||
@@ -26,6 +26,7 @@ import { openDeleteUserModal } from '~/components/Manage/User/delete-user.modal'
|
||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
@@ -180,11 +181,9 @@ const ManageUsersPage = () => {
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
if (!session?.user.isAdmin) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
const result = checkForSessionOrAskForLogin(ctx, session, () => session?.user.isAdmin == true);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
|
||||
@@ -20,6 +20,7 @@ import { openCreateInviteModal } from '~/components/Manage/User/Invite/create-in
|
||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
@@ -152,11 +153,9 @@ const useStyles = createStyles(() => ({
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
if (!session?.user.isAdmin) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
const result = checkForSessionOrAskForLogin(ctx, session, () => session?.user.isAdmin == true);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
|
||||
@@ -71,7 +71,6 @@ const SettingsComponent = ({
|
||||
boardsData: RouterOutputs['boards']['all'];
|
||||
}) => {
|
||||
const languagesData = languages.map((language) => ({
|
||||
image: 'https://img.icons8.com/clouds/256/000000/futurama-bender.png',
|
||||
label: language.originalName,
|
||||
description: language.translatedName,
|
||||
value: language.shortName,
|
||||
@@ -199,8 +198,8 @@ const SelectItem = forwardRef<HTMLDivElement, ItemProps>(
|
||||
({ image, label, description, country, ...others }: ItemProps, ref) => (
|
||||
<div ref={ref} {...others}>
|
||||
<Group noWrap>
|
||||
<span className={`fi fi-${country?.toLowerCase()}`}></span>
|
||||
|
||||
{country !== 'CROWDIN' && <span className={`fi fi-${country?.toLowerCase()}`}></span>}
|
||||
{country === 'CROWDIN' && <img src={'https://support.crowdin.com/assets/logos/crowdin-dark-symbol.png'} alt={label} width={16} height={16} />}
|
||||
<div>
|
||||
<Text size="sm">{label}</Text>
|
||||
<Text size="xs" opacity={0.65}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { JsdelivrIconsRepository } from '~/tools/server/images/jsdelivr-icons-repository';
|
||||
import { LocalIconsRepository } from '~/tools/server/images/local-icons-repository';
|
||||
import { UnpkgIconsRepository } from '~/tools/server/images/unpkg-icons-repository';
|
||||
import { GitHubIconsRepository } from '~/tools/server/images/github-icons-repository';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
@@ -8,8 +9,8 @@ export const iconRouter = createTRPCRouter({
|
||||
all: publicProcedure.query(async () => {
|
||||
const respositories = [
|
||||
new LocalIconsRepository(),
|
||||
new JsdelivrIconsRepository(
|
||||
JsdelivrIconsRepository.tablerRepository,
|
||||
new GitHubIconsRepository(
|
||||
GitHubIconsRepository.walkxcode,
|
||||
'Walkxcode Dashboard Icons',
|
||||
'Walkxcode on Github'
|
||||
),
|
||||
|
||||
@@ -13,7 +13,7 @@ export const timezoneRouter = createTRPCRouter({
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const timezone = GeoTz.find(input.latitude, input.longitude);
|
||||
const timezone = await GeoTz.find(input.latitude, input.longitude);
|
||||
return Array.isArray(timezone) ? timezone[0] : timezone;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
|
||||
const citySchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
country: z.string(),
|
||||
country_code: z.string(),
|
||||
country: z.string().optional(),
|
||||
country_code: z.string().optional(),
|
||||
latitude: z.number(),
|
||||
longitude: z.number(),
|
||||
population: z.number().optional(),
|
||||
|
||||
@@ -2,7 +2,6 @@ export type Language = {
|
||||
shortName: string;
|
||||
originalName: string;
|
||||
translatedName: string;
|
||||
emoji: string;
|
||||
|
||||
/**
|
||||
* The country identified b<y the ISO-3166 alpha 2 code:
|
||||
@@ -18,7 +17,6 @@ export const languages = [
|
||||
shortName: 'de',
|
||||
originalName: 'Deutsch',
|
||||
translatedName: 'German',
|
||||
emoji: '🇩🇪',
|
||||
country: 'DE',
|
||||
locale: 'de',
|
||||
},
|
||||
@@ -26,7 +24,6 @@ export const languages = [
|
||||
shortName: 'en',
|
||||
originalName: 'English',
|
||||
translatedName: 'English',
|
||||
emoji: '🇬🇧',
|
||||
country: 'GB',
|
||||
locale: 'en-gb',
|
||||
},
|
||||
@@ -35,7 +32,6 @@ export const languages = [
|
||||
shortName: 'da',
|
||||
originalName: 'Dansk',
|
||||
translatedName: 'Danish',
|
||||
emoji: '🇩🇰',
|
||||
country: 'DK',
|
||||
locale: 'da',
|
||||
},
|
||||
@@ -44,7 +40,6 @@ export const languages = [
|
||||
shortName: 'he',
|
||||
originalName: 'עברית',
|
||||
translatedName: 'Hebrew',
|
||||
emoji: '🇮🇱',
|
||||
country: 'IL',
|
||||
locale: 'he',
|
||||
},
|
||||
@@ -52,7 +47,6 @@ export const languages = [
|
||||
shortName: 'es',
|
||||
originalName: 'Español',
|
||||
translatedName: 'Spanish',
|
||||
emoji: '🇪🇸',
|
||||
country: 'ES',
|
||||
locale: 'es',
|
||||
},
|
||||
@@ -60,7 +54,6 @@ export const languages = [
|
||||
shortName: 'fr',
|
||||
originalName: 'Français',
|
||||
translatedName: 'French',
|
||||
emoji: '🇫🇷',
|
||||
country: 'FR',
|
||||
locale: 'fr',
|
||||
},
|
||||
@@ -68,7 +61,6 @@ export const languages = [
|
||||
shortName: 'it',
|
||||
originalName: 'Italiano',
|
||||
translatedName: 'Italian',
|
||||
emoji: '🇮🇹',
|
||||
country: 'IT',
|
||||
locale: 'it',
|
||||
},
|
||||
@@ -76,7 +68,6 @@ export const languages = [
|
||||
shortName: 'ja',
|
||||
originalName: '日本語',
|
||||
translatedName: 'Japanese',
|
||||
emoji: '🇯🇵',
|
||||
country: 'JP',
|
||||
locale: 'ja',
|
||||
},
|
||||
@@ -84,24 +75,14 @@ export const languages = [
|
||||
shortName: 'ko',
|
||||
originalName: '한국어',
|
||||
translatedName: 'Korean',
|
||||
emoji: '🇰🇷',
|
||||
country: 'KR',
|
||||
locale: 'ko',
|
||||
},
|
||||
{
|
||||
shortName: 'lol',
|
||||
originalName: 'LOLCAT',
|
||||
translatedName: 'LOLCAT',
|
||||
emoji: '🐱',
|
||||
country: 'LOL',
|
||||
locale: 'en-gb',
|
||||
},
|
||||
// Norwegian
|
||||
{
|
||||
shortName: 'no',
|
||||
originalName: 'Norsk',
|
||||
translatedName: 'Norwegian',
|
||||
emoji: '🇳🇴',
|
||||
country: 'NO',
|
||||
locale: 'nb',
|
||||
},
|
||||
@@ -110,7 +91,6 @@ export const languages = [
|
||||
shortName: 'sk',
|
||||
originalName: 'Slovenčina',
|
||||
translatedName: 'Slovak',
|
||||
emoji: '🇸🇰',
|
||||
country: 'SK',
|
||||
locale: 'sk',
|
||||
},
|
||||
@@ -118,7 +98,6 @@ export const languages = [
|
||||
shortName: 'nl',
|
||||
originalName: 'Nederlands',
|
||||
translatedName: 'Dutch',
|
||||
emoji: '🇳🇱',
|
||||
country: 'NL',
|
||||
locale: 'nl',
|
||||
},
|
||||
@@ -126,7 +105,6 @@ export const languages = [
|
||||
shortName: 'pl',
|
||||
originalName: 'Polski',
|
||||
translatedName: 'Polish',
|
||||
emoji: '🇵🇱',
|
||||
country: 'PL',
|
||||
locale: 'pl',
|
||||
},
|
||||
@@ -134,7 +112,6 @@ export const languages = [
|
||||
shortName: 'pt',
|
||||
originalName: 'Português',
|
||||
translatedName: 'Portuguese',
|
||||
emoji: '🇵🇹',
|
||||
country: 'PT',
|
||||
locale: 'pt',
|
||||
},
|
||||
@@ -142,7 +119,6 @@ export const languages = [
|
||||
shortName: 'ru',
|
||||
originalName: 'Русский',
|
||||
translatedName: 'Russian',
|
||||
emoji: '🇷🇺',
|
||||
country: 'RU',
|
||||
locale: 'ru',
|
||||
},
|
||||
@@ -150,7 +126,6 @@ export const languages = [
|
||||
shortName: 'sl',
|
||||
originalName: 'Slovenščina',
|
||||
translatedName: 'Slovenian',
|
||||
emoji: '🇸🇮',
|
||||
country: 'SI',
|
||||
locale: 'sl',
|
||||
},
|
||||
@@ -158,7 +133,6 @@ export const languages = [
|
||||
shortName: 'sv',
|
||||
originalName: 'Svenska',
|
||||
translatedName: 'Swedish',
|
||||
emoji: '🇸🇪',
|
||||
country: 'SE',
|
||||
locale: 'sv',
|
||||
},
|
||||
@@ -166,7 +140,6 @@ export const languages = [
|
||||
shortName: 'uk',
|
||||
originalName: 'Українська',
|
||||
translatedName: 'Ukrainian',
|
||||
emoji: '🇺🇦',
|
||||
country: 'UA',
|
||||
locale: 'uk',
|
||||
},
|
||||
@@ -175,22 +148,28 @@ export const languages = [
|
||||
shortName: 'vi',
|
||||
originalName: 'Tiếng Việt',
|
||||
translatedName: 'Vietnamese',
|
||||
emoji: '🇻🇳',
|
||||
country: 'VN',
|
||||
locale: 'vi',
|
||||
},
|
||||
// Chinese (Simplified)
|
||||
{
|
||||
shortName: 'zh',
|
||||
shortName: 'cn',
|
||||
originalName: '中文',
|
||||
translatedName: 'Chinese',
|
||||
emoji: '🇨🇳',
|
||||
translatedName: 'Chinese (Simplified)',
|
||||
country: 'CN',
|
||||
locale: 'zh-cn',
|
||||
},
|
||||
// Chinese (Traditional)
|
||||
{
|
||||
shortName: 'tw',
|
||||
originalName: '中文(台灣)',
|
||||
translatedName: 'Chinese (Traditional)',
|
||||
country: 'TW',
|
||||
locale: 'zh-tw',
|
||||
},
|
||||
{
|
||||
originalName: 'Ελληνικά',
|
||||
translatedName: 'Greek',
|
||||
emoji: '🇬🇷',
|
||||
country: 'GR',
|
||||
shortName: 'gr',
|
||||
locale: 'el',
|
||||
@@ -199,7 +178,6 @@ export const languages = [
|
||||
shortName: 'tr',
|
||||
originalName: 'Türkçe',
|
||||
translatedName: 'Turkish',
|
||||
emoji: '🇹🇷',
|
||||
country: 'TR',
|
||||
locale: 'tr',
|
||||
},
|
||||
@@ -207,7 +185,6 @@ export const languages = [
|
||||
shortName: 'lv',
|
||||
originalName: 'Latvian',
|
||||
translatedName: 'Latvian',
|
||||
emoji: '🇱🇻',
|
||||
country: 'LV',
|
||||
locale: 'lv',
|
||||
},
|
||||
@@ -215,7 +192,6 @@ export const languages = [
|
||||
shortName: 'hr',
|
||||
originalName: 'Hrvatski',
|
||||
translatedName: 'Croatian',
|
||||
emoji: '🇭🇷',
|
||||
country: 'HR',
|
||||
locale: 'hr',
|
||||
},
|
||||
@@ -224,10 +200,17 @@ export const languages = [
|
||||
shortName: 'hu',
|
||||
originalName: 'Magyar',
|
||||
translatedName: 'Hungarian',
|
||||
emoji: '🇭🇺',
|
||||
country: 'HU',
|
||||
locale: 'hu',
|
||||
},
|
||||
// Crowdin Live translate
|
||||
{
|
||||
shortName: 'cr',
|
||||
originalName: 'Crowdin',
|
||||
translatedName: '(Live translation)',
|
||||
country: 'CROWDIN',
|
||||
locale: 'cr',
|
||||
},
|
||||
] as const satisfies Readonly<Language[]>;
|
||||
|
||||
export const getLanguageByCode = (code: string | null) =>
|
||||
|
||||
85
src/tools/server/images/github-icons-repository.ts
Normal file
85
src/tools/server/images/github-icons-repository.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
AbstractIconRepository,
|
||||
NormalizedIcon,
|
||||
NormalizedIconRepositoryResult,
|
||||
} from './abstract-icons-repository';
|
||||
|
||||
export class GitHubIconsRepository extends AbstractIconRepository {
|
||||
static readonly walkxcode = {
|
||||
api: 'https://api.github.com/repos/walkxcode/dashboard-icons/git/trees/main?recursive=true',
|
||||
blob: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/{0}/{1}',
|
||||
} as GitHubRepositoryUrl;
|
||||
|
||||
constructor(
|
||||
private readonly repository: GitHubRepositoryUrl,
|
||||
private readonly displayName: string,
|
||||
copyright: string
|
||||
) {
|
||||
super(copyright);
|
||||
}
|
||||
|
||||
protected async fetchInternally(): Promise<NormalizedIconRepositoryResult> {
|
||||
const response = await fetch(this.repository.api, {
|
||||
|
||||
});
|
||||
const body = (await response.json()) as GitHubRepo;
|
||||
|
||||
const normalizedEntries = body.tree
|
||||
.filter((file) => !['banner.png', 'logo.png'].some((x) => file.path.includes(x)))
|
||||
.filter((file) => ['.png', '.svg'].some((x) => file.path.endsWith(x)))
|
||||
.sort((a, b) => {
|
||||
if (a.path.endsWith('.svg') && b.path.endsWith('.png')) {
|
||||
return -1;
|
||||
}
|
||||
if (a.path.endsWith('.png') && b.path.endsWith('.svg')) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map((file): NormalizedIcon => {
|
||||
const fileNameParts = file.path.split('/');
|
||||
const fileName = fileNameParts[fileNameParts.length - 1];
|
||||
const extensions = fileName.split('.')[1];
|
||||
return {
|
||||
url: this.repository.blob.replace('{0}', extensions).replace('{1}', fileName),
|
||||
name: fileName,
|
||||
size: file.size ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
entries: normalizedEntries,
|
||||
count: normalizedEntries.length,
|
||||
success: true,
|
||||
name: this.displayName,
|
||||
copyright: this.copyright,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type GitHubRepositoryUrl = {
|
||||
api: string;
|
||||
blob: string;
|
||||
};
|
||||
|
||||
|
||||
export interface GitHubRepo {
|
||||
sha: string;
|
||||
url: string;
|
||||
tree: Tree[];
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export interface Tree {
|
||||
path: string;
|
||||
mode: string;
|
||||
type: Type;
|
||||
sha: string;
|
||||
url: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export enum Type {
|
||||
Blob = 'blob',
|
||||
Tree = 'tree',
|
||||
}
|
||||
@@ -5,11 +5,6 @@ import {
|
||||
} from './abstract-icons-repository';
|
||||
|
||||
export class JsdelivrIconsRepository extends AbstractIconRepository {
|
||||
static readonly tablerRepository = {
|
||||
api: 'https://data.jsdelivr.com/v1/packages/gh/walkxcode/dashboard-icons@main?structure=flat',
|
||||
blob: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/{0}/{1}',
|
||||
} as JsdelivrRepositoryUrl;
|
||||
|
||||
static readonly papirusRepository = {
|
||||
api: 'https://data.jsdelivr.com/v1/packages/gh/PapirusDevelopmentTeam/papirus_icons@master?structure=flat',
|
||||
blob: 'https://cdn.jsdelivr.net/gh/PapirusDevelopmentTeam/papirus_icons/src/{1}',
|
||||
@@ -35,6 +30,7 @@ export class JsdelivrIconsRepository extends AbstractIconRepository {
|
||||
const normalizedEntries = body.files
|
||||
.filter((file) => !['_banner.png', '_logo.png'].some((x) => file.name.includes(x)))
|
||||
.filter((file) => ['.png', '.svg'].some((x) => file.name.endsWith(x)))
|
||||
|
||||
.map((file): NormalizedIcon => {
|
||||
const fileNameParts = file.name.split('/');
|
||||
const fileName = fileNameParts[fileNameParts.length - 1];
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
GetServerSidePropsResult,
|
||||
PreviewData,
|
||||
} from 'next';
|
||||
|
||||
import { Session } from 'next-auth';
|
||||
|
||||
import { ParsedUrlQuery } from 'querystring';
|
||||
|
||||
export const checkForSessionOrAskForLogin = (
|
||||
@@ -12,23 +14,30 @@ export const checkForSessionOrAskForLogin = (
|
||||
session: Session | null,
|
||||
accessCallback: () => boolean
|
||||
): GetServerSidePropsResult<any> | undefined => {
|
||||
if (!session?.user) {
|
||||
console.log('detected logged out user!');
|
||||
const permitted = accessCallback();
|
||||
|
||||
// user is logged in but does not have the required access
|
||||
if (session?.user && !permitted) {
|
||||
return {
|
||||
props: {},
|
||||
redirect: {
|
||||
destination: `/auth/login?redirectAfterLogin=${context.resolvedUrl}`,
|
||||
permanent: false,
|
||||
},
|
||||
destination: '/401',
|
||||
permanent: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!accessCallback()) {
|
||||
return {
|
||||
props: {},
|
||||
notFound: true,
|
||||
};
|
||||
// user *may* be logged in and permitted
|
||||
if (permitted) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
// user is logged out and needs to sign in
|
||||
return {
|
||||
props: {},
|
||||
redirect: {
|
||||
destination: `/auth/login?redirectAfterLogin=${context.resolvedUrl}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export const adGuardApiStatsResponseSchema = z.object({
|
||||
num_replaced_safebrowsing: z.number().min(0),
|
||||
num_replaced_safesearch: z.number().min(0),
|
||||
num_replaced_parental: z.number().min(0),
|
||||
avg_processing_time: z.number().min(0).max(1),
|
||||
avg_processing_time: z.number().min(0),
|
||||
});
|
||||
|
||||
export const adGuardApiStatusResponseSchema = z.object({
|
||||
|
||||
@@ -51,3 +51,5 @@ export const manageNamespaces = [
|
||||
export const loginNamespaces = ['authentication/login'];
|
||||
|
||||
export const pageNotFoundNamespaces = ['layout/errors/not-found'];
|
||||
|
||||
export const pageAccessDeniedNamespaces = ['layout/errors/access-denied'];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Stack, Text, createStyles } from '@mantine/core';
|
||||
import { useElementSize } from '@mantine/hooks';
|
||||
import { IconClock } from '@tabler/icons-react';
|
||||
import dayjs from 'dayjs';
|
||||
import advancedFormat from 'dayjs/plugin/advancedFormat';
|
||||
import timezones from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { useSession } from 'next-auth/react';
|
||||
@@ -13,6 +14,7 @@ import { api } from '~/utils/api';
|
||||
import { defineWidget } from '../helper';
|
||||
import { IWidget } from '../widgets';
|
||||
|
||||
dayjs.extend(advancedFormat);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezones);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user