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:
10
src/utils/asyncForEach.ts
Normal file
10
src/utils/asyncForEach.ts
Normal 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
96
src/utils/auth.ts
Normal 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
78
src/utils/datetime.ts
Normal 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
28
src/utils/graphics.ts
Normal 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
20
src/utils/html.ts
Normal 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
110
src/utils/i18n.ts
Normal 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
80
src/utils/identity.ts
Normal 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
46
src/utils/image.ts
Normal 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
23
src/utils/listFormat.ts
Normal 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
22
src/utils/location.ts
Normal 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
70
src/utils/poiIcons.ts
Normal 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
80
src/utils/share.ts
Normal 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
36
src/utils/upload.ts
Normal 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
35
src/utils/username.ts
Normal 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
7
src/utils/validators.ts
Normal 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.";
|
||||
}
|
||||
Reference in New Issue
Block a user