485 lines
28 KiB
Rust
485 lines
28 KiB
Rust
use leptos::html::Input;
|
|
use leptos::prelude::*;
|
|
use web_sys::SubmitEvent;
|
|
|
|
use super::context::use_auth;
|
|
use crate::i18n::use_i18n;
|
|
|
|
#[component]
|
|
pub fn RegisterForm() -> impl IntoView {
|
|
let auth = use_auth();
|
|
let i18n = use_i18n();
|
|
|
|
// Store contexts in StoredValue to avoid move issues
|
|
let auth_stored = StoredValue::new(auth);
|
|
let i18n_stored = StoredValue::new(i18n);
|
|
|
|
let (email, set_email) = signal(String::new());
|
|
let (username, set_username) = signal(String::new());
|
|
let (password, set_password) = signal(String::new());
|
|
let (confirm_password, set_confirm_password) = signal(String::new());
|
|
let (display_name, set_display_name) = signal(String::new());
|
|
let (show_password, set_show_password) = signal(false);
|
|
let (show_confirm_password, set_show_confirm_password) = signal(false);
|
|
|
|
let email_ref = NodeRef::<Input>::new();
|
|
let username_ref = NodeRef::<Input>::new();
|
|
let password_ref = NodeRef::<Input>::new();
|
|
let confirm_password_ref = NodeRef::<Input>::new();
|
|
|
|
let password_strength = Memo::new(move |_| {
|
|
let pwd = password.get();
|
|
if pwd.is_empty() {
|
|
return ("", "");
|
|
}
|
|
|
|
let mut score = 0;
|
|
let mut feedback = Vec::new();
|
|
|
|
if pwd.len() >= 8 {
|
|
score += 1;
|
|
} else {
|
|
feedback.push("At least 8 characters");
|
|
}
|
|
|
|
if pwd.chars().any(|c| c.is_uppercase()) {
|
|
score += 1;
|
|
} else {
|
|
feedback.push("One uppercase letter");
|
|
}
|
|
|
|
if pwd.chars().any(|c| c.is_lowercase()) {
|
|
score += 1;
|
|
} else {
|
|
feedback.push("One lowercase letter");
|
|
}
|
|
|
|
if pwd.chars().any(|c| c.is_numeric()) {
|
|
score += 1;
|
|
} else {
|
|
feedback.push("One number");
|
|
}
|
|
|
|
if pwd.chars().any(|c| !c.is_alphanumeric()) {
|
|
score += 1;
|
|
} else {
|
|
feedback.push("One special character");
|
|
}
|
|
|
|
let strength = match score {
|
|
0..=1 => ("Very Weak", "bg-red-500"),
|
|
2 => ("Weak", "bg-orange-500"),
|
|
3 => ("Fair", "bg-yellow-500"),
|
|
4 => ("Good", "bg-blue-500"),
|
|
5 => ("Strong", "bg-green-500"),
|
|
_ => ("Strong", "bg-green-500"),
|
|
};
|
|
|
|
(strength.0, strength.1)
|
|
});
|
|
|
|
let passwords_match = move || {
|
|
let pwd = password.get();
|
|
let confirm = confirm_password.get();
|
|
pwd == confirm && !pwd.is_empty()
|
|
};
|
|
|
|
let form_is_valid = move || {
|
|
!email.get().is_empty()
|
|
&& !username.get().is_empty()
|
|
&& !password.get().is_empty()
|
|
&& passwords_match()
|
|
&& password.get().len() >= 8
|
|
};
|
|
|
|
let on_submit = move |ev: SubmitEvent| {
|
|
ev.prevent_default();
|
|
|
|
if form_is_valid() {
|
|
let email_val = email.get();
|
|
let username_val = username.get();
|
|
let password_val = password.get();
|
|
let display_name_val = if display_name.get().is_empty() {
|
|
None
|
|
} else {
|
|
Some(display_name.get())
|
|
};
|
|
|
|
(auth_stored.get_value().0.actions.register)(
|
|
email_val,
|
|
username_val,
|
|
password_val,
|
|
display_name_val,
|
|
);
|
|
}
|
|
};
|
|
|
|
let toggle_password_visibility = move |_| {
|
|
set_show_password.update(|show| *show = !*show);
|
|
};
|
|
|
|
let toggle_confirm_password_visibility = move |_| {
|
|
set_show_confirm_password.update(|show| *show = !*show);
|
|
};
|
|
|
|
let clear_error = move |_| {
|
|
(auth_stored.get_value().0.actions.clear_error)();
|
|
};
|
|
|
|
view! {
|
|
<div class="w-full max-w-md mx-auto">
|
|
<div class="bg-white shadow-lg rounded-lg p-8">
|
|
<div class="text-center mb-8">
|
|
<h2 class="text-3xl font-bold text-gray-900">{move || i18n_stored.get_value().t("create-account")}</h2>
|
|
<p class="text-gray-600 mt-2">{move || i18n_stored.get_value().t("join-us-today")}</p>
|
|
</div>
|
|
|
|
<Show when=move || auth_stored.get_value().0.error().is_some()>
|
|
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm text-red-800">
|
|
{move || auth_stored.get_value().0.error().unwrap_or_default()}
|
|
</p>
|
|
</div>
|
|
<div class="ml-auto pl-3">
|
|
<button
|
|
type="button"
|
|
class="inline-flex text-red-400 hover:text-red-600"
|
|
on:click=clear_error
|
|
>
|
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
|
|
<form on:submit=on_submit class="space-y-6">
|
|
<div>
|
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{move || i18n_stored.get_value().t("email-address")}
|
|
</label>
|
|
<input
|
|
node_ref=email_ref
|
|
type="email"
|
|
id="email"
|
|
name="email"
|
|
required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder=move || i18n_stored.get_value().t("enter-email")
|
|
prop:value=email
|
|
on:input=move |ev| set_email.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="username" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{move || i18n_stored.get_value().t("username")}
|
|
</label>
|
|
<input
|
|
node_ref=username_ref
|
|
type="text"
|
|
id="username"
|
|
name="username"
|
|
required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder=move || i18n_stored.get_value().t("enter-username")
|
|
prop:value=username
|
|
on:input=move |ev| set_username.set(event_target_value(&ev))
|
|
/>
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
{move || i18n_stored.get_value().t("username-format")}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="display_name" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{move || i18n_stored.get_value().t("display-name")}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="display_name"
|
|
name="display_name"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder=move || i18n_stored.get_value().t("how-should-we-call-you")
|
|
prop:value=display_name
|
|
on:input=move |ev| set_display_name.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{move || i18n_stored.get_value().t("password")}
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
node_ref=password_ref
|
|
type=move || if show_password.get() { "text" } else { "password" }
|
|
id="password"
|
|
name="password"
|
|
required
|
|
class="w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder=move || i18n_stored.get_value().t("enter-password")
|
|
prop:value=password
|
|
on:input=move |ev| set_password.set(event_target_value(&ev))
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
on:click=toggle_password_visibility
|
|
>
|
|
<Show
|
|
when=move || show_password.get()
|
|
fallback=move || view! {
|
|
<svg class="h-5 w-5 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
</svg>
|
|
}
|
|
>
|
|
<svg class="h-5 w-5 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"/>
|
|
</svg>
|
|
</Show>
|
|
</button>
|
|
</div>
|
|
|
|
<Show when=move || !password.get().is_empty()>
|
|
<div class="mt-2">
|
|
<div class="flex items-center justify-between text-sm">
|
|
<span class="text-gray-600">{move || i18n_stored.get_value().t("password-strength")}</span>
|
|
<span class=move || format!("font-medium {}", match password_strength.get().0 {
|
|
"Very Weak" => "text-red-600",
|
|
"Weak" => "text-orange-600",
|
|
"Fair" => "text-yellow-600",
|
|
"Good" => "text-blue-600",
|
|
"Strong" => "text-green-600",
|
|
_ => "text-gray-600",
|
|
})>
|
|
{move || {
|
|
let strength = password_strength.get().0;
|
|
match strength {
|
|
"Very Weak" => i18n_stored.get_value().t("very-weak"),
|
|
"Weak" => i18n_stored.get_value().t("weak"),
|
|
"Fair" => i18n_stored.get_value().t("fair"),
|
|
"Good" => i18n_stored.get_value().t("good"),
|
|
"Strong" => i18n_stored.get_value().t("strong"),
|
|
_ => strength.to_string(),
|
|
}
|
|
}}
|
|
</span>
|
|
</div>
|
|
<div class="mt-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
<div
|
|
class=move || format!("h-full transition-all duration-300 {}", password_strength.get().1)
|
|
style=move || {
|
|
let width = match password_strength.get().0 {
|
|
"Very Weak" => "20%",
|
|
"Weak" => "40%",
|
|
"Fair" => "60%",
|
|
"Good" => "80%",
|
|
"Strong" => "100%",
|
|
_ => "0%",
|
|
};
|
|
format!("width: {}", width)
|
|
}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
{move || i18n_stored.get_value().t("password-requirements")}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="confirm-password" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{move || i18n_stored.get_value().t("confirm-password")}
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
node_ref=confirm_password_ref
|
|
type=move || if show_confirm_password.get() { "text" } else { "password" }
|
|
id="confirm_password"
|
|
name="confirm_password"
|
|
required
|
|
class=move || format!("w-full px-3 py-2 pr-10 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 {}",
|
|
if confirm_password.get().is_empty() {
|
|
"border-gray-300"
|
|
} else if passwords_match() {
|
|
"border-green-300"
|
|
} else {
|
|
"border-red-300"
|
|
}
|
|
)
|
|
placeholder=move || i18n_stored.get_value().t("confirm-password")
|
|
prop:value=confirm_password
|
|
on:input=move |ev| set_confirm_password.set(event_target_value(&ev))
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
on:click=toggle_confirm_password_visibility
|
|
>
|
|
<Show
|
|
when=move || show_confirm_password.get()
|
|
fallback=move || view! {
|
|
<svg class="h-5 w-5 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
</svg>
|
|
}
|
|
>
|
|
<svg class="h-5 w-5 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"/>
|
|
</svg>
|
|
</Show>
|
|
</button>
|
|
</div>
|
|
|
|
<Show when=move || !confirm_password.get().is_empty()>
|
|
<div class="mt-1 flex items-center">
|
|
<Show
|
|
when=move || passwords_match()
|
|
fallback=move || view! {
|
|
<svg class="h-4 w-4 text-red-500 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
<span class="text-sm text-red-600">{move || i18n_stored.get_value().t("passwords-dont-match")}</span>
|
|
}
|
|
>
|
|
<svg class="h-4 w-4 text-green-500 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
</svg>
|
|
<span class="text-sm text-green-600">{move || i18n_stored.get_value().t("passwords-match")}</span>
|
|
</Show>
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<input
|
|
id="terms"
|
|
name="terms"
|
|
type="checkbox"
|
|
required
|
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
/>
|
|
<label for="terms" class="ml-2 block text-sm text-gray-900">
|
|
{move || i18n_stored.get_value().t("i-agree-to-the")}{" "}
|
|
<a href="/terms" class="text-blue-600 hover:text-blue-500">
|
|
{move || i18n_stored.get_value().t("terms-of-service")}
|
|
</a>
|
|
{" "}{move || i18n_stored.get_value().t("and")}{" "}
|
|
<a href="/privacy" class="text-blue-600 hover:text-blue-500">
|
|
{move || i18n_stored.get_value().t("privacy-policy")}
|
|
</a>
|
|
</label>
|
|
</div>
|
|
|
|
<div>
|
|
<button
|
|
type="submit"
|
|
disabled=move || auth_stored.get_value().0.is_loading() || !form_is_valid()
|
|
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Show
|
|
when=move || auth_stored.get_value().0.is_loading()
|
|
fallback=move || view! { {i18n_stored.get_value().t("create-account")} }
|
|
>
|
|
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
{i18n_stored.get_value().t("creating-account")}
|
|
</Show>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="mt-6">
|
|
<div class="relative">
|
|
<div class="absolute inset-0 flex items-center">
|
|
<div class="w-full border-t border-gray-300"/>
|
|
</div>
|
|
<div class="relative flex justify-center text-sm">
|
|
<span class="px-2 bg-white text-gray-500">{move || i18n_stored.get_value().t("continue-with")}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 grid grid-cols-3 gap-3">
|
|
<button
|
|
type="button"
|
|
class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
|
on:click=move |_| {
|
|
// TODO: Implement OAuth registration
|
|
if let Err(e) = window().location().set_href("/api/auth/oauth/google/authorize") {
|
|
web_sys::console::error_1(&format!("Failed to redirect to Google OAuth: {:?}", e).into());
|
|
}
|
|
}
|
|
>
|
|
<svg class="h-5 w-5" viewBox="0 0 24 24">
|
|
<path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
<path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
<path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
<path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
</svg>
|
|
<span class="sr-only">Sign up with Google</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
|
on:click=move |_| {
|
|
// TODO: Implement OAuth registration
|
|
if let Err(e) = window().location().set_href("/api/auth/oauth/github/authorize") {
|
|
web_sys::console::error_1(&format!("Failed to redirect to GitHub OAuth: {:?}", e).into());
|
|
}
|
|
}
|
|
>
|
|
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
</svg>
|
|
<span class="sr-only">Sign up with GitHub</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
|
on:click=move |_| {
|
|
// TODO: Implement OAuth registration
|
|
if let Err(e) = window().location().set_href("/api/auth/oauth/discord/authorize") {
|
|
web_sys::console::error_1(&format!("Failed to redirect to Discord OAuth: {:?}", e).into());
|
|
}
|
|
}
|
|
>
|
|
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
|
</svg>
|
|
<span class="sr-only">Sign up with Discord</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 text-center">
|
|
<p class="text-sm text-gray-600">
|
|
{move || i18n_stored.get_value().t("already-have-account")}{" "}
|
|
<a href="/auth/login" class="font-medium text-blue-600 hover:text-blue-500">
|
|
{move || i18n_stored.get_value().t("sign-in")}
|
|
</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|