embystream 0.0.36

Another Emby streaming application (frontend/backend separation) written in Rust.
Documentation
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";

import { ApiError, getRegistrationSettings } from "@/api/client";
import AuthStageShell from "@/components/blocks/AuthStageShell.vue";
import { useDocumentLocale } from "@/composables/useDocumentLocale";
import { useTheme } from "@/composables/useTheme";
import { useSessionStore } from "@/stores/session";

const { t, tm } = useI18n();
const router = useRouter();
const sessionStore = useSessionStore();
const { theme, themePreference, cycleThemePreference } = useTheme();

const form = reactive({
  username: "",
  email: "",
  password: "",
});
const pending = ref(false);
const errorMessage = ref("");
const registrationEnabled = ref(true);

useDocumentLocale();

const nextThemePreferenceLabel = computed(() => {
  if (themePreference.value === "system") {
    return t("common.themeLight");
  }

  if (themePreference.value === "light") {
    return t("common.themeDark");
  }

  return t("common.themeSystem");
});

const accentLabel = computed(() =>
  t("common.themeSwitchTo", { mode: nextThemePreferenceLabel.value }),
);

const submitDisabled = computed(
  () =>
    pending.value ||
    !registrationEnabled.value ||
    !form.username.trim() ||
    !form.password.trim() ||
    !form.email.trim(),
);
const authSignals = computed(() => tm("auth.signals") as string[]);

onMounted(async () => {
  try {
    const response = await getRegistrationSettings();
    registrationEnabled.value = response.registration_enabled;
  } catch {
    registrationEnabled.value = false;
  }

  if (!registrationEnabled.value) {
    await router.replace({
      name: "login",
      query: { notice: "registration_closed" },
    });
  }
});

async function handleSwitch() {
  await router.push({ name: "login" });
}

async function submit() {
  if (submitDisabled.value) {
    return;
  }

  pending.value = true;
  errorMessage.value = "";

  try {
    await sessionStore.signUp({
      username: form.username,
      email: form.email,
      password: form.password,
    });
    await router.push({ name: "login" });
  } catch (error) {
    errorMessage.value =
      error instanceof ApiError ? error.message : t("errors.registerFailed");
  } finally {
    pending.value = false;
  }
}
</script>

<template>
  <AuthStageShell
    :body="t('auth.register.body')"
    :eyebrow="t('auth.register.eyebrow')"
    :panel-body="t('auth.panelBody')"
    :panel-title="t('auth.panelTitle')"
    :signals="authSignals"
    :story-body="t('auth.wallBody')"
    :story-label="t('auth.storyLabel')"
    :story-title="t('auth.wallTitle')"
    :switch-label="t('auth.register.switch')"
    switch-to="/login"
    :theme-label="accentLabel"
    :theme-mode="theme"
    :theme-preference="themePreference"
    :title="t('auth.register.title')"
    @switch="handleSwitch"
    @toggle-theme="cycleThemePreference"
  >
    <form class="auth-form" @submit.prevent="submit">
      <label>
        <span>{{ t("auth.register.usernameLabel") }}</span>
        <input v-model="form.username" autocomplete="username" type="text" />
      </label>
      <label>
        <span>{{ t("auth.register.emailLabel") }}</span>
        <input v-model="form.email" autocomplete="email" type="email" />
      </label>
      <label>
        <span>{{ t("auth.register.passwordLabel") }}</span>
        <input
          v-model="form.password"
          autocomplete="new-password"
          type="password"
        />
      </label>

      <p class="auth-form__hint">{{ t("auth.register.helper") }}</p>
      <p
        v-if="errorMessage"
        class="auth-form__feedback auth-form__feedback--error"
      >
        {{ errorMessage }}
      </p>

      <button
        :disabled="submitDisabled"
        class="auth-form__submit"
        type="submit"
      >
        {{ pending ? t("common.loading") : t("auth.register.submit") }}
      </button>
    </form>
  </AuthStageShell>
</template>

<style scoped>
.auth-form {
  display: grid;
  gap: 0.95rem;
}

.auth-form label {
  display: grid;
  gap: 0.5rem;
}

.auth-form span {
  color: var(--text-main);
  font-size: 0.88rem;
  font-weight: 600;
}

.auth-form input {
  border-color: var(--field-border);
  background: var(--field-bg);
}

.auth-form__hint,
.auth-form__feedback {
  margin: 0;
  font-size: 0.88rem;
  line-height: 1.55;
}

.auth-form__hint {
  color: var(--text-faint);
}

.auth-form__feedback--error {
  color: var(--signal-red);
}

.auth-form__submit {
  min-height: 2.95rem;
  border-color: transparent;
  background: var(--button-primary-bg);
  color: #ffffff;
  font-weight: 700;
}

.auth-form__submit:hover:not(:disabled) {
  background: var(--button-primary-hover);
  border-color: transparent;
}

.auth-form__submit:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}
</style>