Finnish translation
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import { type RouteDefinition, Router } from "@solidjs/router";
|
import { type RouteDefinition, Router } from "@solidjs/router";
|
||||||
import { FileRoutes } from "@solidjs/start/router";
|
import { FileRoutes } from "@solidjs/start/router";
|
||||||
import { MetaProvider } from "@solidjs/meta";
|
import { Meta, MetaProvider } from "@solidjs/meta";
|
||||||
import { Suspense } from "solid-js";
|
import { createEffect, Suspense } from "solid-js";
|
||||||
import { querySession } from "./auth";
|
import { querySession } from "./auth";
|
||||||
import Auth from "./components/Context";
|
import Auth from "./components/Context";
|
||||||
import Nav from "./components/Nav";
|
import Nav from "./components/Nav";
|
||||||
import ErrorNotification from "./components/Error";
|
import ErrorNotification from "./components/Error";
|
||||||
|
import { language, t } from "~/i18n";
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
|
|
||||||
export const route: RouteDefinition = {
|
export const route: RouteDefinition = {
|
||||||
@@ -13,10 +14,17 @@ export const route: RouteDefinition = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
createEffect(() => {
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.documentElement.lang = language();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router
|
<Router
|
||||||
root={props => (
|
root={props => (
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
|
<Meta name="description" content={t("meta.description")} />
|
||||||
<Auth>
|
<Auth>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { action, query, redirect } from "@solidjs/router";
|
import { action, query, redirect } from "@solidjs/router";
|
||||||
|
import { getLanguageFromFormData, getTranslations } from "~/i18n";
|
||||||
import { getSession, passwordLogin } from "./server";
|
import { getSession, passwordLogin } from "./server";
|
||||||
|
|
||||||
// Define routes that require being logged in
|
// Define routes that require being logged in
|
||||||
const PROTECTED_ROUTES = ["/"];
|
const PROTECTED_ROUTES = ["/"];
|
||||||
|
|
||||||
const isProtected = (path: string) =>
|
const isProtected = (path: string) =>
|
||||||
PROTECTED_ROUTES.some(route =>
|
PROTECTED_ROUTES.some((route) =>
|
||||||
route.endsWith("/*")
|
route.endsWith("/*")
|
||||||
? path.startsWith(route.slice(0, -2))
|
? path.startsWith(route.slice(0, -2))
|
||||||
: path === route || path.startsWith(route + "/")
|
: path === route || path.startsWith(route + "/"),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const querySession = query(async (path: string) => {
|
export const querySession = query(async (path: string) => {
|
||||||
@@ -22,11 +23,13 @@ export const querySession = query(async (path: string) => {
|
|||||||
|
|
||||||
export const formLogin = action(async (formData: FormData) => {
|
export const formLogin = action(async (formData: FormData) => {
|
||||||
"use server";
|
"use server";
|
||||||
|
const lang = getLanguageFromFormData(formData);
|
||||||
|
const translations = getTranslations(lang);
|
||||||
const email = formData.get("email");
|
const email = formData.get("email");
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
if (typeof email !== "string" || typeof password !== "string")
|
if (typeof email !== "string" || typeof password !== "string")
|
||||||
return new Error("Email and password are required");
|
return new Error(translations["errors.requiredEmailPassword"]);
|
||||||
return await passwordLogin(email.trim().toLowerCase(), password);
|
return await passwordLogin(email.trim().toLowerCase(), password, lang);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const logout = action(async () => {
|
export const logout = action(async () => {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { redirect } from "@solidjs/router";
|
|||||||
import { useSession } from "vinxi/http";
|
import { useSession } from "vinxi/http";
|
||||||
import { getRandomValues, subtle, timingSafeEqual } from "crypto";
|
import { getRandomValues, subtle, timingSafeEqual } from "crypto";
|
||||||
import { createUser, findUser } from "./db";
|
import { createUser, findUser } from "./db";
|
||||||
|
import type { Language } from "~/i18n";
|
||||||
|
import { getTranslations } from "~/i18n";
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -43,10 +45,15 @@ async function createHash(password: string) {
|
|||||||
return `${saltHex}:${hash}`;
|
return `${saltHex}:${hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkPassword(storedPassword: string, providedPassword: string) {
|
async function checkPassword(
|
||||||
|
storedPassword: string,
|
||||||
|
providedPassword: string,
|
||||||
|
lang: Language,
|
||||||
|
) {
|
||||||
|
const translations = getTranslations(lang);
|
||||||
const [storedSalt, storedHash] = storedPassword.split(":");
|
const [storedSalt, storedHash] = storedPassword.split(":");
|
||||||
if (!storedSalt || !storedHash)
|
if (!storedSalt || !storedHash)
|
||||||
throw new Error("Invalid stored password format");
|
throw new Error(translations["errors.invalidStoredPasswordFormat"]);
|
||||||
const key = await subtle.deriveBits(
|
const key = await subtle.deriveBits(
|
||||||
{
|
{
|
||||||
name: "PBKDF2",
|
name: "PBKDF2",
|
||||||
@@ -66,20 +73,22 @@ async function checkPassword(storedPassword: string, providedPassword: string) {
|
|||||||
const hash = Buffer.from(key);
|
const hash = Buffer.from(key);
|
||||||
const stored = Buffer.from(storedHash, "hex");
|
const stored = Buffer.from(storedHash, "hex");
|
||||||
if (stored.length !== hash.length || !timingSafeEqual(stored, hash))
|
if (stored.length !== hash.length || !timingSafeEqual(stored, hash))
|
||||||
throw new Error("Invalid email or password");
|
throw new Error(translations["errors.invalidEmailOrPassword"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function passwordLogin(email: string, password: string) {
|
export async function passwordLogin(
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
lang: Language = "en",
|
||||||
|
) {
|
||||||
|
const translations = getTranslations(lang);
|
||||||
let user = await findUser({ email });
|
let user = await findUser({ email });
|
||||||
if (!user)
|
if (!user)
|
||||||
user = await createUser({
|
user = await createUser({
|
||||||
email,
|
email,
|
||||||
password: await createHash(password),
|
password: await createHash(password),
|
||||||
});
|
});
|
||||||
else if (!user.password)
|
else if (!user.password) throw new Error(translations["errors.oauthOnly"]);
|
||||||
throw new Error(
|
else await checkPassword(user.password, password, lang);
|
||||||
"Account exists via OAuth. Sign in with your OAuth provider",
|
|
||||||
);
|
|
||||||
else await checkPassword(user.password, password);
|
|
||||||
return createSession(user);
|
return createSession(user);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
|
import { t } from "~/i18n";
|
||||||
|
|
||||||
export default function Counter() {
|
export default function Counter() {
|
||||||
const [count, setCount] = createSignal(0);
|
const [count, setCount] = createSignal(0);
|
||||||
@@ -8,7 +9,7 @@ export default function Counter() {
|
|||||||
class="w-52 rounded-full bg-gray-100 border-2 border-gray-300 focus:border-gray-400 py-4"
|
class="w-52 rounded-full bg-gray-100 border-2 border-gray-300 focus:border-gray-400 py-4"
|
||||||
onclick={() => setCount(prev => prev + 1)}
|
onclick={() => setCount(prev => prev + 1)}
|
||||||
>
|
>
|
||||||
Clicks: {count()}
|
{t("counter.clicks")}: {count()}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useSearchParams } from "@solidjs/router";
|
import { useSearchParams } from "@solidjs/router";
|
||||||
import { createEffect, onCleanup, Show } from "solid-js";
|
import { createEffect, onCleanup, Show } from "solid-js";
|
||||||
|
import { t } from "~/i18n";
|
||||||
import { X } from "./Icons";
|
import { X } from "./Icons";
|
||||||
|
|
||||||
export default function ErrorNotification() {
|
export default function ErrorNotification() {
|
||||||
@@ -17,7 +18,7 @@ export default function ErrorNotification() {
|
|||||||
{msg => (
|
{msg => (
|
||||||
<aside class="flex items-start gap-3 fixed bottom-4 left-4 max-w-sm bg-red-50 border border-red-200 rounded-xl p-4 shadow-lg z-50 transition-all duration-300 text-sm">
|
<aside class="flex items-start gap-3 fixed bottom-4 left-4 max-w-sm bg-red-50 border border-red-200 rounded-xl p-4 shadow-lg z-50 transition-all duration-300 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<strong class="font-medium text-red-800">Error</strong>
|
<strong class="font-medium text-red-800">{t("error.title")}</strong>
|
||||||
<p class="text-red-700 mt-1 select-text">{msg}</p>
|
<p class="text-red-700 mt-1 select-text">{msg}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMatch } from "@solidjs/router";
|
import { useMatch } from "@solidjs/router";
|
||||||
import { Show } from "solid-js";
|
import { Show } from "solid-js";
|
||||||
import { useAuth } from "~/components/Context";
|
import { useAuth } from "~/components/Context";
|
||||||
|
import { language, setLanguage, t } from "~/i18n";
|
||||||
|
|
||||||
export default function Nav() {
|
export default function Nav() {
|
||||||
const { signedIn, logout } = useAuth();
|
const { signedIn, logout } = useAuth();
|
||||||
@@ -11,40 +12,58 @@ export default function Nav() {
|
|||||||
<nav class="fixed top-0 left-0 w-full bg-sky-800 shadow-sm z-50 flex items-center justify-between py-3 px-4 font-medium text-sm">
|
<nav class="fixed top-0 left-0 w-full bg-sky-800 shadow-sm z-50 flex items-center justify-between py-3 px-4 font-medium text-sm">
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class={`px-3 py-2 text-sky-100 uppercase transition-colors duration-200 border-b-2 ${
|
class={`px-3 py-2 text-sky-100 uppercase transition-colors duration-200 border-b-2 ${isHome() ? "border-sky-300 text-white" : "border-transparent hover:text-white"
|
||||||
isHome() ? "border-sky-300 text-white" : "border-transparent hover:text-white"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Home
|
{t("nav.home")}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/about"
|
href="/about"
|
||||||
class={`px-3 py-2 text-sky-100 uppercase transition-colors duration-200 border-b-2 ${
|
class={`px-3 py-2 text-sky-100 uppercase transition-colors duration-200 border-b-2 ${isAbout() ? "border-sky-300 text-white" : "border-transparent hover:text-white"
|
||||||
isAbout() ? "border-sky-300 text-white" : "border-transparent hover:text-white"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
About
|
{t("nav.about")}
|
||||||
</a>
|
</a>
|
||||||
<Show
|
<div class="ml-auto flex items-center gap-2">
|
||||||
when={signedIn()}
|
<div class="flex items-center gap-1 rounded-md border border-sky-700 bg-sky-700/40 p-1">
|
||||||
fallback={
|
|
||||||
<a
|
|
||||||
href="/login"
|
|
||||||
class="ml-auto px-4 py-2 text-sky-100 bg-sky-700 border border-sky-600 rounded-md hover:bg-sky-600 hover:text-white focus:outline-none transition-colors duration-200"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<form action={logout} method="post" class="ml-auto">
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="button"
|
||||||
class="px-4 py-2 text-sky-100 bg-sky-700 border border-sky-600 rounded-md hover:bg-sky-600 hover:text-white focus:outline-none transition-colors duration-200"
|
onclick={() => setLanguage("fi")}
|
||||||
|
class={`px-2 py-1 text-xs rounded ${language() === "fi" ? "bg-sky-200 text-sky-900" : "text-sky-100 hover:text-white"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
Sign Out
|
{t("nav.language.fi")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
<button
|
||||||
</Show>
|
type="button"
|
||||||
|
onclick={() => setLanguage("en")}
|
||||||
|
class={`px-2 py-1 text-xs rounded ${language() === "en" ? "bg-sky-200 text-sky-900" : "text-sky-100 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t("nav.language.en")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Show
|
||||||
|
when={signedIn()}
|
||||||
|
fallback={
|
||||||
|
<a
|
||||||
|
href="/login"
|
||||||
|
class="px-4 py-2 text-sky-100 bg-sky-700 border border-sky-600 rounded-md hover:bg-sky-600 hover:text-white focus:outline-none transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{t("nav.login")}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form action={logout} method="post">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-4 py-2 text-sky-100 bg-sky-700 border border-sky-600 rounded-md hover:bg-sky-600 hover:text-white focus:outline-none transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{t("nav.signOut")}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
105
ui/src/i18n.ts
Normal file
105
ui/src/i18n.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { createSignal } from "solid-js";
|
||||||
|
import { isServer } from "solid-js/web";
|
||||||
|
|
||||||
|
export type Language = "fi" | "en";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "ui-language";
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
"nav.home": "Home",
|
||||||
|
"nav.about": "About",
|
||||||
|
"nav.login": "Login",
|
||||||
|
"nav.signOut": "Sign Out",
|
||||||
|
"nav.language.fi": "FI",
|
||||||
|
"nav.language.en": "EN",
|
||||||
|
"meta.description": "SolidStart with-auth example",
|
||||||
|
"home.title": "Home",
|
||||||
|
"home.heading": "Hello World",
|
||||||
|
"home.signedInAs": "You are signed in as",
|
||||||
|
"home.logoAlt": "logo",
|
||||||
|
"about.title": "About",
|
||||||
|
"about.apiVersion": "API version",
|
||||||
|
"about.loading": "Loading...",
|
||||||
|
"login.title": "Sign In",
|
||||||
|
"login.heading": "Sign in",
|
||||||
|
"login.orContinueWith": "Or continue with",
|
||||||
|
"login.signInWithDiscord": "Sign in with Discord",
|
||||||
|
"login.email": "Email",
|
||||||
|
"login.password": "Password",
|
||||||
|
"login.submit": "Submit",
|
||||||
|
"notFound.title": "Page Not Found",
|
||||||
|
"notFound.heading": "Not Found",
|
||||||
|
"notFound.message": "Sorry, the page you’re looking for doesn't exist",
|
||||||
|
"notFound.goHome": "Go Home",
|
||||||
|
"error.title": "Error",
|
||||||
|
"counter.clicks": "Clicks",
|
||||||
|
"errors.requiredEmailPassword": "Email and password are required",
|
||||||
|
"errors.invalidStoredPasswordFormat": "Invalid stored password format",
|
||||||
|
"errors.invalidEmailOrPassword": "Invalid email or password",
|
||||||
|
"errors.oauthOnly":
|
||||||
|
"Account exists via OAuth. Sign in with your OAuth provider",
|
||||||
|
},
|
||||||
|
fi: {
|
||||||
|
"nav.home": "Etusivu",
|
||||||
|
"nav.about": "Tietoja",
|
||||||
|
"nav.login": "Kirjaudu",
|
||||||
|
"nav.signOut": "Kirjaudu ulos",
|
||||||
|
"nav.language.fi": "FI",
|
||||||
|
"nav.language.en": "EN",
|
||||||
|
"meta.description": "SolidStart with-auth -esimerkki",
|
||||||
|
"home.title": "Etusivu",
|
||||||
|
"home.heading": "Hei maailma",
|
||||||
|
"home.signedInAs": "Olet kirjautunut käyttäjänä",
|
||||||
|
"home.logoAlt": "logo",
|
||||||
|
"about.title": "Tietoja",
|
||||||
|
"about.apiVersion": "API-versio",
|
||||||
|
"about.loading": "Ladataan...",
|
||||||
|
"login.title": "Kirjaudu",
|
||||||
|
"login.heading": "Kirjaudu sisään",
|
||||||
|
"login.orContinueWith": "Tai jatka",
|
||||||
|
"login.signInWithDiscord": "Kirjaudu Discordilla",
|
||||||
|
"login.email": "Sähköposti",
|
||||||
|
"login.password": "Salasana",
|
||||||
|
"login.submit": "Lähetä",
|
||||||
|
"notFound.title": "Sivua ei löytynyt",
|
||||||
|
"notFound.heading": "Ei löytynyt",
|
||||||
|
"notFound.message": "Valitettavasti etsimääsi sivua ei ole olemassa",
|
||||||
|
"notFound.goHome": "Takaisin etusivulle",
|
||||||
|
"error.title": "Virhe",
|
||||||
|
"counter.clicks": "Klikkauksia",
|
||||||
|
"errors.requiredEmailPassword": "Sähköposti ja salasana vaaditaan",
|
||||||
|
"errors.invalidStoredPasswordFormat":
|
||||||
|
"Tallennetun salasanan muoto on virheellinen",
|
||||||
|
"errors.invalidEmailOrPassword": "Virheellinen sähköposti tai salasana",
|
||||||
|
"errors.oauthOnly": "Tili löytyy OAuthin kautta. Kirjaudu OAuth-palvelulla",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type TranslationKey = keyof typeof translations.en;
|
||||||
|
|
||||||
|
export const normalizeLanguage = (value: unknown): Language =>
|
||||||
|
value === "fi" ? "fi" : "en";
|
||||||
|
|
||||||
|
const [language, setLanguageSignal] = createSignal<Language>("en");
|
||||||
|
|
||||||
|
if (!isServer) {
|
||||||
|
const stored = normalizeLanguage(localStorage.getItem(STORAGE_KEY));
|
||||||
|
setLanguageSignal(stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setLanguage = (lang: Language) => {
|
||||||
|
setLanguageSignal(lang);
|
||||||
|
if (!isServer) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, lang);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { language };
|
||||||
|
|
||||||
|
export const getTranslations = (lang: Language) => translations[lang];
|
||||||
|
|
||||||
|
export const t = (key: TranslationKey) => translations[language()][key];
|
||||||
|
|
||||||
|
export const getLanguageFromFormData = (formData: FormData): Language =>
|
||||||
|
normalizeLanguage(formData.get("lang"));
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
|
import { t } from "~/i18n";
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<main class="text-center">
|
<main class="text-center">
|
||||||
<Title>Page Not Found</Title>
|
<Title>{t("notFound.title")}</Title>
|
||||||
<h1>Not Found</h1>
|
<h1>{t("notFound.heading")}</h1>
|
||||||
Sorry, the page you’re looking for doesn't exist
|
{t("notFound.message")}
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="px-4 py-2 border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-100 transition-colors duration-200"
|
class="px-4 py-2 border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-100 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
Go Home
|
{t("notFound.goHome")}
|
||||||
</a>
|
</a>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
import { createAsync } from "@solidjs/router";
|
import { createAsync } from "@solidjs/router";
|
||||||
import { Show } from "solid-js";
|
import { Show } from "solid-js";
|
||||||
import Counter from "~/components/Counter";
|
|
||||||
import { queryApiVersion } from "~/api";
|
import { queryApiVersion } from "~/api";
|
||||||
|
import { t } from "~/i18n";
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
const apiVersion = createAsync(() => queryApiVersion());
|
const apiVersion = createAsync(() => queryApiVersion());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<Title>Tietoja</Title>
|
<Title>{t("about.title")}</Title>
|
||||||
<p class="text-gray-700 text-center">
|
<p class="text-gray-700 text-center">
|
||||||
API version:{" "}
|
{t("about.apiVersion")}:
|
||||||
<Show when={apiVersion()} fallback="Loading...">
|
<Show when={apiVersion()} fallback={t("about.loading")}>
|
||||||
{apiVersion()}
|
{apiVersion()}
|
||||||
</Show>
|
</Show>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
import { useAuth } from "~/components/Context";
|
import { useAuth } from "~/components/Context";
|
||||||
|
import { t } from "~/i18n";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { session } = useAuth();
|
const { session } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<Title>Home</Title>
|
<Title>{t("home.title")}</Title>
|
||||||
<h1 class="text-center">Hello World</h1>
|
<h1 class="text-center">{t("home.heading")}</h1>
|
||||||
<img src="/favicon.svg" alt="logo" class="w-28" />
|
<img src="/favicon.svg" alt={t("home.logoAlt")} class="w-28" />
|
||||||
You are signed in as <b class="font-medium">{session()?.email}</b>
|
{t("home.signedInAs")} <b class="font-medium">{session()?.email}</b>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,20 @@ import { Show } from "solid-js";
|
|||||||
import { useOAuthLogin } from "start-oauth";
|
import { useOAuthLogin } from "start-oauth";
|
||||||
import { formLogin } from "~/auth";
|
import { formLogin } from "~/auth";
|
||||||
import { Discord } from "~/components/Icons";
|
import { Discord } from "~/components/Icons";
|
||||||
|
import { language, t } from "~/i18n";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const login = useOAuthLogin();
|
const login = useOAuthLogin();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<Title>Sign In</Title>
|
<Title>{t("login.title")}</Title>
|
||||||
<h1>Sign in</h1>
|
<h1>{t("login.heading")}</h1>
|
||||||
<div class="space-y-6 font-medium">
|
<div class="space-y-6 font-medium">
|
||||||
<PasswordLogin />
|
<PasswordLogin />
|
||||||
<div class="flex items-center w-full text-xs">
|
<div class="flex items-center w-full text-xs">
|
||||||
<span class="flex-grow bg-gray-300 h-[1px]" />
|
<span class="flex-grow bg-gray-300 h-[1px]" />
|
||||||
<span class="flex-grow-0 mx-2 text-gray-500">Or continue with</span>
|
<span class="flex-grow-0 mx-2 text-gray-500">{t("login.orContinueWith")}</span>
|
||||||
<span class="flex-grow bg-gray-300 h-[1px]" />
|
<span class="flex-grow bg-gray-300 h-[1px]" />
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
@@ -25,7 +26,7 @@ export default function Login() {
|
|||||||
class="group w-full px-3 py-2 bg-white border border-gray-200 rounded-lg hover:bg-[#5865F2] hover:border-gray-300 focus:outline-none transition-colors duration-300 flex items-center justify-center gap-2.5 text-gray-700 hover:text-white"
|
class="group w-full px-3 py-2 bg-white border border-gray-200 rounded-lg hover:bg-[#5865F2] hover:border-gray-300 focus:outline-none transition-colors duration-300 flex items-center justify-center gap-2.5 text-gray-700 hover:text-white"
|
||||||
>
|
>
|
||||||
<Discord class="h-5 fill-[#5865F2] group-hover:fill-white duration-300" />
|
<Discord class="h-5 fill-[#5865F2] group-hover:fill-white duration-300" />
|
||||||
Sign in with Discord
|
{t("login.signInWithDiscord")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -37,8 +38,9 @@ function PasswordLogin() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form action={formLogin} method="post" class="space-y-4 space-x-12">
|
<form action={formLogin} method="post" class="space-y-4 space-x-12">
|
||||||
|
<input type="hidden" name="lang" value={language()} />
|
||||||
<label for="email" class="block text-left w-full">
|
<label for="email" class="block text-left w-full">
|
||||||
Email
|
{t("login.email")}
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
@@ -50,7 +52,7 @@ function PasswordLogin() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label for="password" class="block text-left w-full">
|
<label for="password" class="block text-left w-full">
|
||||||
Password
|
{t("login.password")}
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
@@ -67,7 +69,7 @@ function PasswordLogin() {
|
|||||||
disabled={submission.pending}
|
disabled={submission.pending}
|
||||||
class="w-full px-4 py-2 bg-gradient-to-r from-sky-600 to-blue-600 text-white rounded-lg hover:from-sky-700 hover:to-blue-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-300 shadow-lg shadow-sky-500/25"
|
class="w-full px-4 py-2 bg-gradient-to-r from-sky-600 to-blue-600 text-white rounded-lg hover:from-sky-700 hover:to-blue-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-300 shadow-lg shadow-sky-500/25"
|
||||||
>
|
>
|
||||||
Submit
|
{t("login.submit")}
|
||||||
</button>
|
</button>
|
||||||
<Show when={submission.error} keyed>
|
<Show when={submission.error} keyed>
|
||||||
{({ message }) => <p class="text-red-600 mt-2 text-xs text-center">{message}</p>}
|
{({ message }) => <p class="text-red-600 mt-2 text-xs text-center">{message}</p>}
|
||||||
|
|||||||
Reference in New Issue
Block a user