Skip to content

Commit

Permalink
Implemented user sign-in and sign-out. (#82)
Browse files Browse the repository at this point in the history
Co-authored-by: Francis Pion <francis.pion@akinox.com>
  • Loading branch information
Utar94 and fpion-akinox authored Apr 22, 2024
1 parent d6a108e commit 94bd929
Show file tree
Hide file tree
Showing 31 changed files with 528 additions and 317 deletions.
13 changes: 13 additions & 0 deletions src/api/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import sleep from "./sleep";
import type { CurrentUser, SignInPayload } from "@/types/account";
import { useUserStore } from "@/stores/user";

export async function signIn(payload: SignInPayload): Promise<CurrentUser> {
await sleep(2500);
const users = useUserStore();
return users.signIn(payload);
}

export async function signOut(): Promise<void> {
await sleep(2500);
}
3 changes: 3 additions & 0 deletions src/api/sleep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
7 changes: 4 additions & 3 deletions src/components/layout/AppNavbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ watchEffect(() => {
</ul>
</li>
<template v-if="user">
<!-- TODO(fpion): implement
<li class="nav-item">
<RouterLink :to="{ name: 'ArticleList' }" class="nav-link"><font-awesome-icon icon="fas fa-carrot" /> {{ t("articles.title.list") }}</RouterLink>
</li>
Expand All @@ -117,7 +118,7 @@ watchEffect(() => {
</li>
<li class="nav-item">
<RouterLink :to="{ name: 'TaxList' }" class="nav-link"><font-awesome-icon icon="fas fa-sack-dollar" /> {{ t("taxes.title.list") }}</RouterLink>
</li>
</li> -->
</template>
</ul>

Expand Down Expand Up @@ -163,13 +164,13 @@ watchEffect(() => {
</ul>
</li>
</template>
<!-- <template v-else>
<template v-else>
<li class="nav-item">
<RouterLink :to="{ name: 'SignIn' }" class="nav-link">
<font-awesome-icon icon="fas fa-arrow-right-to-bracket" /> {{ t("users.signIn.title") }}
</RouterLink>
</li>
</template> -->
</template>
</ul>
</div>
</div>
Expand Down
142 changes: 142 additions & 0 deletions src/components/shared/AppInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<script setup lang="ts">
import { TarInput, inputUtils, parsingUtils, type InputOptions, type InputStatus } from "logitar-vue3-ui";
import { computed, ref } from "vue";
import { nanoid } from "nanoid";
import { useField } from "vee-validate";
import { useI18n } from "vue-i18n";
import type { ShowStatus, ValidationListeners, ValidationRules, ValidationType } from "@/types/validation";
import { isNullOrWhiteSpace } from "@/helpers/stringUtils";
const { isDateTimeInput, isNumericInput, isTextualInput } = inputUtils;
const { parseBoolean, parseNumber } = parsingUtils;
const { t } = useI18n();
const props = withDefaults(
defineProps<
InputOptions & {
rules?: ValidationRules;
showStatus?: ShowStatus;
validation?: ValidationType;
}
>(),
{
id: () => nanoid(),
showStatus: "touched",
validation: "client",
},
);
const inputRef = ref<InstanceType<typeof TarInput> | null>(null);
const describedBy = computed<string>(() => `${props.id}_invalid-feedback`);
const inputMax = computed<number | string | undefined>(() => (props.validation === "server" || isDateTimeInput(props.type) ? props.max : undefined));
const inputMin = computed<number | string | undefined>(() => (props.validation === "server" || isDateTimeInput(props.type) ? props.min : undefined));
const inputName = computed<string>(() => props.name ?? props.id);
const inputRequired = computed<boolean | "label">(() => (parseBoolean(props.required) ? (props.validation === "server" ? true : "label") : false));
const validationRules = computed<ValidationRules>(() => {
const rules: ValidationRules = {};
if (props.validation === "server") {
return rules;
}
const required: boolean | undefined = parseBoolean(props.required);
if (required) {
rules.required = true;
}
const max: number | undefined = parseNumber(props.max);
const min: number | undefined = parseNumber(props.min);
if (isNumericInput(props.type)) {
if (max) {
rules.max_value = max;
}
if (min) {
rules.min_value = min;
}
} else if (isTextualInput(props.type)) {
if (max) {
rules.max_length = max;
}
if (min) {
rules.min_length = min;
}
}
if (!isNullOrWhiteSpace(props.pattern)) {
rules.regex = props.pattern;
}
switch (props.type) {
case "email":
rules.email = true;
break;
case "url":
rules.url = true;
break;
}
return { ...rules, ...props.rules };
});
const displayLabel = computed<string>(() => (props.label ? t(props.label).toLowerCase() : inputName.value));
const { errorMessage, handleChange, meta, value } = useField<string>(inputName, validationRules, {
initialValue: props.modelValue,
label: displayLabel,
});
const inputStatus = computed<InputStatus | undefined>(() => {
if (props.showStatus === "always" || (props.showStatus === "touched" && (meta.dirty || meta.touched))) {
return props.status ?? (props.validation === "server" ? undefined : meta.valid ? "valid" : "invalid");
}
return undefined;
});
const validationListeners = computed<ValidationListeners>(() => ({
blur: handleChange,
change: handleChange,
input: errorMessage.value ? handleChange : (e: unknown) => handleChange(e, false),
}));
function focus(): void {
inputRef.value?.focus();
}
defineExpose({ focus });
</script>

<template>
<TarInput
:described-by="describedBy"
:disabled="disabled"
:floating="floating"
:id="id"
:label="label ? t(label) : undefined"
:max="inputMax"
:min="inputMin"
:model-value="validation === 'server' ? modelValue : value"
:name="name"
:pattern="validation === 'server' ? pattern : undefined"
:placeholder="placeholder ? t(placeholder) : undefined"
:plaintext="plaintext"
:readonly="readonly"
ref="inputRef"
:required="inputRequired"
:size="size"
:status="inputStatus"
:step="step"
:type="type"
v-on="validationListeners"
>
<template #before>
<slot name="before"></slot>
</template>
<template #prepend>
<slot name="prepend"></slot>
</template>
<template #append>
<slot name="append"></slot>
</template>
<template #after>
<div v-if="errorMessage" class="invalid-feedback" :id="describedBy">{{ errorMessage }}</div>
<slot name="after"></slot>
</template>
</TarInput>
</template>
78 changes: 78 additions & 0 deletions src/components/users/PasswordInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import AppInput from "@/components/shared/AppInput.vue";
import type { ConfirmedParams, ValidationRules } from "@/types/validation";
import type { PasswordSettings } from "@/types/settings";
const { t } = useI18n();
const props = withDefaults(
defineProps<{
confirm?: ConfirmedParams<string>;
id?: string;
label?: string;
modelValue?: string;
required?: boolean | string;
settings?: PasswordSettings;
}>(),
{
id: "password",
label: "users.password.label",
},
);
const inputRef = ref<InstanceType<typeof AppInput> | null>();
const rules = computed<ValidationRules>(() => {
const rules: ValidationRules = {};
if (props.confirm) {
rules.confirmed = [props.confirm.value, t(props.confirm.label).toLowerCase()];
} else if (props.settings) {
if (props.settings.minimumLength) {
rules.min_length = props.settings.minimumLength;
}
if (props.settings.uniqueCharacters) {
rules.unique_chars = props.settings.uniqueCharacters;
}
if (props.settings.requireNonAlphanumeric) {
rules.require_non_alphanumeric = true;
}
if (props.settings.requireLowercase) {
rules.require_lowercase = true;
}
if (props.settings.requireUppercase) {
rules.require_uppercase = true;
}
if (props.settings.requireDigit) {
rules.require_digit = true;
}
}
return rules;
});
defineEmits<{
(e: "update:model-value", value?: string): void;
}>();
function focus(): void {
inputRef.value?.focus();
}
defineExpose({ focus });
</script>

<template>
<AppInput
floating
:id="id"
:label="label"
:model-value="modelValue"
:placeholder="label"
ref="inputRef"
:required="required"
:rules="rules"
type="password"
@update:model-value="$emit('update:model-value', $event)"
/>
</template>
46 changes: 46 additions & 0 deletions src/components/users/UsernameInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { computed } from "vue";
import { parsingUtils } from "logitar-vue3-ui";
import AppInput from "@/components/shared/AppInput.vue";
import type { UsernameSettings } from "@/types/settings";
import type { ValidationRules } from "@/types/validation";
const { parseBoolean } = parsingUtils;
const props = defineProps<{
disabled?: boolean | string;
modelValue?: string;
noStatus?: boolean | string;
required?: boolean | string;
settings?: UsernameSettings;
}>();
const rules = computed<ValidationRules>(() => {
const rules: ValidationRules = {};
if (props.settings?.allowedCharacters) {
rules.allowed_characters = props.settings.allowedCharacters;
}
return rules;
});
defineEmits<{
(e: "update:model-value", value?: string): void;
}>();
</script>

<template>
<AppInput
:disabled="disabled"
floating
id="username"
label="users.username"
max="255"
:model-value="modelValue"
placeholder="users.username"
:required="required"
:rules="rules"
:show-status="parseBoolean(noStatus) ? 'never' : undefined"
@update:model-value="$emit('update:model-value', $event)"
/>
</template>
4 changes: 2 additions & 2 deletions src/fontAwesome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { App } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";

import { library } from "@fortawesome/fontawesome-svg-core";
import { faHome, faVial } from "@fortawesome/free-solid-svg-icons";
import { faArrowRightFromBracket, faArrowRightToBracket, faHome, faUser, faVial } from "@fortawesome/free-solid-svg-icons";

library.add(faHome, faVial);
library.add(faArrowRightFromBracket, faArrowRightToBracket, faHome, faUser, faVial);

export default function (app: App) {
app.component("font-awesome-icon", FontAwesomeIcon);
Expand Down
1 change: 1 addition & 0 deletions src/i18n/en/index.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
},
"brand": "Logitar",
"copyright": "© Logitar {version} {year}.",
"loading": "Loading…",
"notFound": {
"help": "The requested page could not be found.",
"lead": "Page Not Found",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/en/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import countries from "./countries.en.json";
import index from "./index.en.json";
import users from "./users.en.json";

export default {
...index,
countries,
users,
};
13 changes: 13 additions & 0 deletions src/i18n/en/users.en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"password": {
"label": "Password"
},
"signIn": {
"failed": "Sign in failed!",
"invalidCredentials": "Please check you provided the correct email username and password.",
"submit": "Sign in",
"title": "Sign In"
},
"signOut": "Sign Out",
"username": "Username"
}
1 change: 1 addition & 0 deletions src/i18n/fr/index.fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
},
"brand": "Logitar",
"copyright": "© Logitar {version} {year}.",
"loading": "Chargement…",
"notFound": {
"help": "La page demandée n’a pu être trouvée.",
"lead": "Page introuvable",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/fr/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import countries from "./countries.fr.json";
import index from "./index.fr.json";
import users from "./users.fr.json";

export default {
...index,
countries,
users,
};
13 changes: 13 additions & 0 deletions src/i18n/fr/users.fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"password": {
"label": "Mot de passe"
},
"signIn": {
"failed": "Connexion échouée !",
"invalidCredentials": "Veuillez vérifier que vous avez entré votre nom d’utilisateur et mot de passe.",
"submit": "Se connecter",
"title": "Connexion"
},
"signOut": "Déconnexion",
"username": "Nom d’utilisateur"
}
Loading

0 comments on commit 94bd929

Please sign in to comment.