build: switch from yarn to npm to manage js dependencies and move js contents to root

yarn v1 is being deprecated and starts to have some issues

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2023-11-14 17:24:42 +01:00
parent 32055122c3
commit 2e72f6faf4
595 changed files with 12078 additions and 7843 deletions

10
src/utils/asyncForEach.ts Normal file
View File

@@ -0,0 +1,10 @@
async function asyncForEach(
array: Array<any>,
callback: (arg0: any, arg1: number, arg2: Array<any>) => void
): Promise<void> {
for (let index = 0; index < array.length; index += 1) {
callback(array[index], index, array);
}
}
export { asyncForEach };

96
src/utils/auth.ts Normal file
View File

@@ -0,0 +1,96 @@
import {
AUTH_ACCESS_TOKEN,
AUTH_REFRESH_TOKEN,
AUTH_USER_ACTOR_ID,
AUTH_USER_EMAIL,
AUTH_USER_ID,
AUTH_USER_ROLE,
USER_LOCALE,
} from "@/constants";
import { ILogin, IToken } from "@/types/login.model";
import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user";
import { UPDATE_CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { ICurrentUserRole } from "@/types/enums";
import { LOGOUT } from "@/graphql/auth";
import { provideApolloClient, useMutation } from "@vue/apollo-composable";
import { apolloClient } from "@/vue-apollo";
export function saveTokenData(obj: IToken): void {
localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken);
localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken);
}
export function saveUserData(obj: ILogin): void {
localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`);
localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
localStorage.setItem(AUTH_USER_ROLE, obj.user.role);
saveTokenData(obj);
}
export function saveLocaleData(locale: string): void {
localStorage.setItem(USER_LOCALE, locale);
}
export function getLocaleData(): string | null {
return localStorage ? localStorage.getItem(USER_LOCALE) : null;
}
export function deleteUserData(): void {
[
AUTH_USER_ID,
AUTH_USER_EMAIL,
AUTH_ACCESS_TOKEN,
AUTH_REFRESH_TOKEN,
AUTH_USER_ACTOR_ID,
AUTH_USER_ROLE,
].forEach((key) => {
localStorage.removeItem(key);
});
}
const { mutate: logoutMutation } = provideApolloClient(apolloClient)(() =>
useMutation(LOGOUT)
);
const { mutate: cleanUserClient } = provideApolloClient(apolloClient)(() =>
useMutation(UPDATE_CURRENT_USER_CLIENT)
);
const { mutate: cleanActorClient } = provideApolloClient(apolloClient)(() =>
useMutation(UPDATE_CURRENT_ACTOR_CLIENT)
);
export async function logout(performServerLogout = true): Promise<void> {
if (performServerLogout) {
logoutMutation({
refreshToken: localStorage.getItem(AUTH_REFRESH_TOKEN),
});
}
cleanUserClient({
id: null,
email: null,
isLoggedIn: false,
role: ICurrentUserRole.USER,
});
cleanActorClient({
id: null,
avatar: null,
preferredUsername: null,
name: null,
});
deleteUserData();
}
export const SELECTED_PROVIDERS: { [key: string]: string } = {
twitter: "Twitter",
discord: "Discord",
facebook: "Facebook",
github: "Github",
gitlab: "Gitlab",
google: "Google",
keycloak: "Keycloak",
ldap: "LDAP",
cas: "CAS",
};

78
src/utils/datetime.ts Normal file
View File

@@ -0,0 +1,78 @@
import type { Locale } from "date-fns";
import { format } from "date-fns";
function localeMonthNames(): string[] {
const monthNames: string[] = [];
for (let i = 0; i < 12; i += 1) {
const d = new Date(2019, i, 1);
const month = d.toLocaleString("default", { month: "long" });
monthNames.push(month);
}
return monthNames;
}
function localeShortWeekDayNames(): string[] {
const weekDayNames: string[] = [];
for (let i = 13; i < 20; i += 1) {
const d = new Date(2019, 9, i);
const weekDay = d.toLocaleString("default", { weekday: "short" });
weekDayNames.push(weekDay);
}
return weekDayNames;
}
// https://stackoverflow.com/a/18650828/10204399
function formatBytes(
bytes: number,
decimals = 2,
locale: string | undefined = undefined
): string {
const formatNumber = (value = 0, unit = "byte") =>
new Intl.NumberFormat(locale, {
style: "unit",
unit,
unitDisplay: "long",
}).format(value);
if (bytes === 0) return formatNumber(0);
if (bytes < 0 || bytes > Number.MAX_SAFE_INTEGER) {
throw new RangeError(
"Number mustn't be negative and be inferior to Number.MAX_SAFE_INTEGER"
);
}
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = [
"byte",
"kilobyte",
"megabyte",
"gigabyte",
"terabyte",
"petabyte",
];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return formatNumber(parseFloat((bytes / k ** i).toFixed(dm)), sizes[i]);
}
function roundToNearestMinute(date = new Date()) {
const minutes = 1;
const ms = 1000 * 60 * minutes;
// 👇️ replace Math.round with Math.ceil to always round UP
return new Date(Math.round(date.getTime() / ms) * ms);
}
function formatDateTimeForEvent(dateTime: Date, locale: Locale): string {
return format(dateTime, "PPp", { locale });
}
export {
localeMonthNames,
localeShortWeekDayNames,
formatBytes,
roundToNearestMinute,
formatDateTimeForEvent,
};

28
src/utils/graphics.ts Normal file
View File

@@ -0,0 +1,28 @@
import random from "lodash/random";
export const randomGradient = (): string => {
const direction = [
"bg-gradient-to-t",
"bg-gradient-to-tr",
"bg-gradient-to-r",
"bg-gradient-to-br",
"bg-gradient-to-b",
"bg-gradient-to-bl",
"bg-gradient-to-l",
"bg-gradient-to-tl",
];
const gradients = [
"from-pink-500 via-red-500 to-yellow-500",
"from-green-400 via-blue-500 to-purple-600",
"from-pink-400 via-purple-300 to-indigo-400",
"from-yellow-300 via-yellow-500 to-yellow-700",
"from-yellow-300 via-green-400 to-green-500",
"from-red-400 via-red-600 to-yellow-400",
"from-green-400 via-green-500 to-blue-700",
"from-yellow-400 via-yellow-500 to-yellow-700",
"from-green-300 via-green-400 to-purple-700",
];
return `${direction[random(0, direction.length - 1)]} ${
gradients[random(0, gradients.length - 1)]
}`;
};

20
src/utils/html.ts Normal file
View File

@@ -0,0 +1,20 @@
export function nl2br(text: string): string {
return text.replace(/(?:\r\n|\r|\n)/g, "<br>");
}
export function htmlToText(html: string) {
const template = document.createElement("template");
const trimmedHTML = html.trim();
template.innerHTML = trimmedHTML;
const text = template.content.textContent;
template.remove();
return text;
}
export const getValueFromMeta = (name: string): string | null => {
const element = document.querySelector(`meta[name="${name}"]`);
if (element && element.getAttribute("content")) {
return element.getAttribute("content");
}
return null;
};

110
src/utils/i18n.ts Normal file
View File

@@ -0,0 +1,110 @@
import { createI18n } from "vue-i18n";
import en from "../i18n/en_US.json";
import langs from "../i18n/langs.json";
import { getLocaleData } from "./auth";
import pluralizationRules from "../i18n/pluralRules";
const DEFAULT_LOCALE = "en_US";
const localeInLocalStorage = getLocaleData();
export const AVAILABLE_LANGUAGES = Object.keys(langs);
let language =
localeInLocalStorage ||
(document.documentElement.getAttribute("lang") as string);
language =
language ||
((window.navigator as any).userLanguage || window.navigator.language).replace(
/-/,
"_"
);
export const locale =
language && Object.prototype.hasOwnProperty.call(langs, language)
? language
: language.split("-")[0];
export const i18n = createI18n({
legacy: false,
locale: locale, // set locale
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// messages, // set locale messages
messages: en, // set locale messages
fallbackLocale: DEFAULT_LOCALE,
fallbackFormat: true,
pluralizationRules,
fallbackRootWithEmptyString: true,
globalInjection: true,
});
const loadedLanguages = [DEFAULT_LOCALE];
function setI18nLanguage(lang: string): string {
i18n.global.locale = lang;
setLanguageInDOM(lang);
return lang;
}
function setLanguageInDOM(lang: string): void {
const fixedLang = lang.replace(/_/g, "-");
const html = document.documentElement;
const documentLang = html.getAttribute("lang");
if (documentLang !== fixedLang) {
html.setAttribute("lang", fixedLang);
}
const direction = ["ar", "ae", "he", "fa", "ku", "ur"].includes(fixedLang)
? "rtl"
: "ltr";
html.setAttribute("dir", direction);
}
function fileForLanguage(matches: Record<string, string>, lang: string) {
if (Object.prototype.hasOwnProperty.call(matches, lang)) {
return matches[lang];
}
return lang;
}
function vueI18NfileForLanguage(lang: string) {
const matches: Record<string, string> = {
fr: "fr_FR",
en: "en_US",
};
return fileForLanguage(matches, lang);
}
export async function loadLanguageAsync(lang: string): Promise<string> {
// If the same language
if (i18n.global.locale === lang) {
return Promise.resolve(setI18nLanguage(lang));
}
// If the language was already loaded
if (loadedLanguages.includes(lang)) {
return Promise.resolve(setI18nLanguage(lang));
}
// If the language hasn't been loaded yet
const newMessages = await import(
`../i18n/${vueI18NfileForLanguage(lang)}.json`
);
i18n.global.setLocaleMessage(lang, newMessages.default);
loadedLanguages.push(lang);
return setI18nLanguage(lang);
}
loadLanguageAsync(locale);
export function formatList(list: string[]): string {
if (window.Intl && Intl.ListFormat) {
const formatter = new Intl.ListFormat(undefined, {
style: "long",
type: "conjunction",
});
return formatter.format(list);
}
return list.join(",");
}

80
src/utils/identity.ts Normal file
View File

@@ -0,0 +1,80 @@
import { AUTH_USER_ACTOR_ID } from "@/constants";
import { UPDATE_CURRENT_ACTOR_CLIENT, IDENTITIES } from "@/graphql/actor";
import { IPerson } from "@/types/actor";
import { ICurrentUser } from "@/types/current-user.model";
import { apolloClient } from "@/vue-apollo";
import {
provideApolloClient,
useLazyQuery,
useMutation,
} from "@vue/apollo-composable";
import { computed } from "vue";
export class NoIdentitiesException extends Error {}
function saveActorData(obj: IPerson): void {
localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`);
}
const {
mutate: updateCurrentActorClient,
onDone: onUpdateCurrentActorClientDone,
} = provideApolloClient(apolloClient)(() =>
useMutation(UPDATE_CURRENT_ACTOR_CLIENT)
);
export async function changeIdentity(identity: IPerson): Promise<void> {
if (!identity.id) return;
console.debug("Changing identity", identity);
updateCurrentActorClient(identity);
if (identity.id) {
console.debug("Saving actor data");
saveActorData(identity);
}
onUpdateCurrentActorClientDone(() => {
console.debug("Updating current actor client");
});
}
const { load: loadIdentities } = provideApolloClient(apolloClient)(() =>
useLazyQuery<{ loggedUser: Pick<ICurrentUser, "actors"> }>(IDENTITIES)
);
/**
* We fetch from localStorage the latest actor ID used,
* then fetch the current identities to set in cache
* the current identity used
*/
export async function initializeCurrentActor(): Promise<void> {
const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID);
console.debug("Initializing current actor", actorId);
try {
const result = await loadIdentities();
if (!result) return;
console.debug("got identities", result);
const identities = computed(() => result.loggedUser?.actors);
console.debug(
"initializing current actor based on identities",
identities.value
);
if (identities.value && identities.value.length < 1) {
console.warn("Logged user has no identities!");
throw new NoIdentitiesException();
}
const activeIdentity =
(identities.value || []).find(
(identity: IPerson | undefined) => identity?.id === actorId
) || ((identities.value || [])[0] as IPerson);
if (activeIdentity) {
await changeIdentity(activeIdentity);
}
} catch (e) {
console.error("Failed to initialize current Actor", e);
}
}

46
src/utils/image.ts Normal file
View File

@@ -0,0 +1,46 @@
import { IMedia } from "@/types/media.model";
export async function buildFileFromIMedia(
obj: IMedia | null | undefined
): Promise<File | null> {
if (!obj) return Promise.resolve(null);
const response = await fetch(obj.url);
const blob = await response.blob();
return new File([blob], obj.name);
}
export function buildFileVariable(
file: File | null,
name: string,
alt?: string
): Record<string, unknown> {
if (!file) return {};
return {
[name]: {
media: {
name: file.name,
alt: alt || file.name,
file,
},
},
};
}
export function readFileAsync(
file: File
): Promise<string | ArrayBuffer | null> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsBinaryString(file);
});
}

23
src/utils/listFormat.ts Normal file
View File

@@ -0,0 +1,23 @@
const shortConjunctionFormatter = new Intl.ListFormat(undefined, {
style: "short",
type: "conjunction",
});
const shortDisjunctionFormatter = new Intl.ListFormat(undefined, {
style: "short",
type: "disjunction",
});
const listFormatAvailable = typeof Intl?.ListFormat === "function";
export const listShortConjunctionFormatter = (list: Array<string>): string => {
return listFormatAvailable
? shortConjunctionFormatter.format(list)
: list.join(",");
};
export const listShortDisjunctionFormatter = (list: Array<string>): string => {
return listFormatAvailable
? shortDisjunctionFormatter.format(list)
: list.join(",");
};

22
src/utils/location.ts Normal file
View File

@@ -0,0 +1,22 @@
import ngeohash from "ngeohash";
const GEOHASH_DEPTH = 9; // put enough accuracy, radius will be used anyway
export const coordsToGeoHash = (
lat: number | undefined,
lon: number | undefined,
depth = GEOHASH_DEPTH
): string | undefined => {
if (lat && lon && depth) {
return ngeohash.encode(lat, lon, GEOHASH_DEPTH);
}
return undefined;
};
export const geoHashToCoords = (
geohash: string | undefined
): { latitude: number; longitude: number } | undefined => {
if (!geohash) return undefined;
const { latitude, longitude } = ngeohash.decode(geohash);
return latitude && longitude ? { latitude, longitude } : undefined;
};

70
src/utils/poiIcons.ts Normal file
View File

@@ -0,0 +1,70 @@
export interface IPOIIcon {
icon: string;
color?: string;
}
interface IPOIIcons {
[key: string]: IPOIIcon;
}
export const poiIcons: IPOIIcons = {
default: {
icon: "map-marker",
color: "#5C6F84",
},
defaultAdministrative: {
icon: "city",
color: "#5c6f84",
},
defaultStreet: {
icon: "road-variant",
color: "#5c6f84",
},
defaultAddress: {
icon: "home",
color: "#5c6f84",
},
place_house: {
icon: "home",
color: "#5c6f84",
},
theatre: {
icon: "drama-masks",
},
parking: {
icon: "parking",
},
police: {
icon: "police-badge",
},
post_office: {
icon: "email",
},
university: {
icon: "school",
},
college: {
icon: "school",
},
park: {
icon: "pine-tree",
},
garden: {
icon: "pine-tree",
},
bicycle_rental: {
icon: "bicycle",
},
hospital: {
icon: "hospital-box",
},
townhall: {
icon: "office-building",
},
toilets: {
icon: "human-male-female",
},
hairdresser: {
icon: "content-cut",
},
};

80
src/utils/share.ts Normal file
View File

@@ -0,0 +1,80 @@
export const twitterShareUrl = (
url: string | undefined,
text: string | undefined
): string | undefined => {
if (!url || !text) return undefined;
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
url
)}&text=${text}`;
};
export const facebookShareUrl = (
url: string | undefined
): string | undefined => {
if (!url) return undefined;
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
url
)}`;
};
export const linkedInShareUrl = (
url: string | undefined,
text: string | undefined
): string | undefined => {
if (!url || !text) return undefined;
return `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(
url
)}&title=${text}`;
};
export const whatsAppShareUrl = (
url: string | undefined,
text: string | undefined
): string | undefined => {
if (!url || !text) return undefined;
return `https://wa.me/?text=${encodeURIComponent(
basicTextToEncode(url, text)
)}`;
};
export const telegramShareUrl = (
url: string | undefined,
text: string | undefined
): string | undefined => {
if (!url || !text) return undefined;
return `https://t.me/share/url?url=${encodeURIComponent(
url
)}&text=${encodeURIComponent(text)}`;
};
export const emailShareUrl = (
url: string | undefined,
text: string | undefined
): string | undefined => {
if (!url || !text) return undefined;
return `mailto:?to=&body=${url}&subject=${text}`;
};
export const diasporaShareUrl = (
url: string | undefined,
text: string | undefined
): string | undefined => {
if (!url || !text) return undefined;
return `https://share.diasporafoundation.org/?title=${encodeURIComponent(
text
)}&url=${encodeURIComponent(url)}`;
};
export const mastodonShareUrl = (
url: string | undefined,
text: string | undefined
): string | undefined => {
if (!url || !text) return undefined;
return `https://toot.kytta.dev/?text=${encodeURIComponent(
basicTextToEncode(url, text)
)}`;
};
const basicTextToEncode = (url: string, text: string): string => {
return `${text}\r\n${url}`;
};

36
src/utils/upload.ts Normal file
View File

@@ -0,0 +1,36 @@
/**
* Async function to upload a file
*/
export function listenFileUpload(): Promise<File> {
return new Promise((resolve, reject) => {
const inputElement = document.createElement("input");
inputElement.type = "file";
inputElement.onchange = () => {
if (inputElement.files && inputElement.files.length > 0) {
resolve(inputElement.files[0]);
}
};
inputElement.onerror = reject;
inputElement.click();
});
}
/**
* Async function to upload a file
*/
export function listenFileUploads(): Promise<FileList> {
return new Promise((resolve, reject) => {
const inputElement = document.createElement("input");
inputElement.type = "file";
inputElement.multiple = true;
inputElement.onchange = () => {
if (inputElement.files && inputElement.files.length > 0) {
resolve(inputElement.files);
}
};
inputElement.onerror = reject;
inputElement.click();
});
}

35
src/utils/username.ts Normal file
View File

@@ -0,0 +1,35 @@
import { IActor } from "@/types/actor";
function convertToUsername(value: string | null): string {
if (!value) return "";
// https://stackoverflow.com/a/37511463
return value
.toLocaleLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/\s{2,}/, " ")
.replace(/ /g, "_")
.replace(/[^a-z0-9_]/g, "")
.replace(/_{2,}/, "");
}
function autoUpdateUsername(
actor: IActor,
newDisplayName: string | null
): IActor {
const actor2 = { ...actor };
const oldUsername = convertToUsername(actor.name);
if (actor.preferredUsername === oldUsername) {
actor2.preferredUsername = convertToUsername(newDisplayName);
}
return actor2;
}
function validateUsername(actor: IActor): boolean {
return actor.preferredUsername === convertToUsername(actor.preferredUsername);
}
export { autoUpdateUsername, convertToUsername, validateUsername };

7
src/utils/validators.ts Normal file
View File

@@ -0,0 +1,7 @@
export function validateEmailField(value: string): boolean | string {
return value.includes("@") || "Invalid e-mail.";
}
export function validateRequiredField(value: unknown): boolean | string {
return !!value || "Required.";
}