721 lines
24 KiB
Vue
721 lines
24 KiB
Vue
<template>
|
|
<div class="ig-media-cards-vue row">
|
|
<div class="row">
|
|
<div class="col-12 col-md-6">
|
|
<div class="input-group mb-1">
|
|
<label
|
|
id="ig-media-cards-search"
|
|
class="input-group-text"
|
|
><FontAwesomeIcon icon="fa-solid fa-magnifying-glass" /></label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
:placeholder="__('Rechercher dans la liste (utilisateur, lien)')"
|
|
:aria-label="__('Rechercher un post instagram dans toute la liste')"
|
|
aria-describedby="ig-media-cards-search"
|
|
v-model="searchText"
|
|
@submit.prevent
|
|
:disabled="ajaxLoading"
|
|
>
|
|
<button
|
|
class="btn btn-secondary"
|
|
type="button"
|
|
:title="__('Efface la recherche')"
|
|
data-bs-toggle="tooltip"
|
|
@click.prevent="searchText = ''"
|
|
:disabled="ajaxLoading"
|
|
>
|
|
<FontAwesomeIcon icon="fa-solid fa-xmark" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="d-flex">
|
|
<div class="input-group mb-1 py-2">
|
|
<strong>{{
|
|
__("Total :total", { total: String(mediaList.length) })
|
|
}}</strong>
|
|
<span
|
|
v-if="ajaxLoading"
|
|
class="px-2"
|
|
>
|
|
<FontAwesomeIcon
|
|
:icon="['fas', 'spinner']"
|
|
spin-pulse
|
|
/>
|
|
</span>
|
|
</div>
|
|
<div class="input-group mb-1">
|
|
<button
|
|
class="btn btn-success"
|
|
:title="__('Ecrase le classement avec cette nouvelle sélection')"
|
|
data-bs-toggle="tooltip"
|
|
:disabled="ajaxLoading"
|
|
@click.prevent="saveMediaList()"
|
|
>
|
|
{{ __("Valider le classement") }}
|
|
</button>
|
|
<button
|
|
class="btn btn-primary"
|
|
:title="__('Force le rechargement des posts depuis Instagram')"
|
|
data-bs-toggle="tooltip"
|
|
@click.prevent="fetchMediaList(true)"
|
|
:disabled="ajaxLoading"
|
|
>
|
|
<FontAwesomeIcon :icon="['fas', 'rotate']" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="sMediaList.length"
|
|
class="ig-media-cards d-flex flex-wrap"
|
|
>
|
|
<div
|
|
class="ig-media d-flex position-relative user-select-none"
|
|
v-for="(igMedia, igMediaIndex) of sMediaList"
|
|
:key="igMediaIndex"
|
|
>
|
|
<label
|
|
:for="`${String(name)}${String(igMediaIndex)}`"
|
|
:class="`d-flex flex-column justify-content-between flex-grow-1 bg-dark text-bg-dark border-secondary p-1 m-1 border border-2 rounded`"
|
|
>
|
|
<div
|
|
class="d-flex flex-column justify-content-between h-100 user-select-none"
|
|
>
|
|
<div class="d-flex flex-column text-center">
|
|
<img
|
|
v-if="mediaFirstPicture(igMedia)"
|
|
class="img-fluid ig-picture-mini"
|
|
:src="String(mediaFirstPicture(igMedia))"
|
|
>
|
|
<div class="row">
|
|
<div class="col text-center">
|
|
<strong
|
|
:class="`text-center px-4 fs-${getMediaRank(igMedia)}`"
|
|
>{{ `#${getMediaRank(igMedia)}` }}</strong>
|
|
</div>
|
|
<div
|
|
v-if="igMedia.like_count"
|
|
class="col text-center"
|
|
>
|
|
<FontAwesomeIcon :icon="['far', 'heart']" />
|
|
<span>{{ igMedia.like_count }}</span>
|
|
</div>
|
|
<div class="col text-center">
|
|
<FontAwesomeIcon :icon="['far', 'message']" />
|
|
<span>{{ igMedia.comments_count }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="row px-2">
|
|
<div class="col text-center">
|
|
<a
|
|
:title="
|
|
__('Voir le profile de @:username sur Instagram', {
|
|
username: igMedia.username,
|
|
})
|
|
"
|
|
data-bs-toggle="tooltip"
|
|
:href="`https://www.instagram.com/${igMedia.username}`"
|
|
target="_blank"
|
|
><strong>@{{ igMedia.username }}</strong></a>
|
|
</div>
|
|
<div class="col text-center">
|
|
<span>{{
|
|
new Date(Date.parse(igMedia.timestamp)).toLocaleDateString()
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
<p>"{{ str_limit(igMedia.caption, 95) }}"</p>
|
|
<div
|
|
class="text-center"
|
|
:title="__('Voir le contenu du post en entier')"
|
|
data-bs-toggle="tooltip"
|
|
>
|
|
<div class="btn-group">
|
|
<button
|
|
class="btn btn-info"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#participation-modal"
|
|
@click.prevent="initCarousel(igMedia)"
|
|
:disabled="ajaxLoading"
|
|
>
|
|
{{ __("Voir le post entier") }}
|
|
</button>
|
|
<a
|
|
class="btn btn-secondary"
|
|
:href="igMedia.permalink"
|
|
target="_blank"
|
|
>{{ __("Voir sur Instagram") }}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button
|
|
:title="__('Exclure ce post du classement')"
|
|
data-bs-toggle="tooltip"
|
|
class="btn btn-sm btn-warning"
|
|
:disabled="ajaxLoading"
|
|
@click.prevent="excludeMedia(igMedia)"
|
|
>
|
|
{{ __("Exclure") }}
|
|
</button>
|
|
<button
|
|
:title="
|
|
__('Exclure tous les posts de @:username', {
|
|
username: igMedia.username,
|
|
})
|
|
"
|
|
data-bs-toggle="tooltip"
|
|
class="btn btn-sm btn-danger"
|
|
:disabled="ajaxLoading"
|
|
@click.prevent="excludeUser(igMedia.username)"
|
|
>
|
|
{{
|
|
__("Exclure @:username", {
|
|
username: igMedia.username,
|
|
})
|
|
}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="ajaxLoading">
|
|
<span class="text-center fs-4">{{ __("Chargement en cours..") }}</span>
|
|
</div>
|
|
<div v-else-if="searchText.length">
|
|
<span class="text-center fs-4">{{
|
|
__("Aucun post Instagram ne correspond à :search", {
|
|
search: searchText,
|
|
})
|
|
}}</span>
|
|
</div>
|
|
<div v-else>
|
|
<span class="text-center fs-4">{{ __("Aucun post Instagram") }}</span>
|
|
</div>
|
|
<div
|
|
id="participation-modal"
|
|
class="modal fade"
|
|
tabindex="-1"
|
|
>
|
|
<div class="modal-dialog modal-xl">
|
|
<div
|
|
v-if="modalMedia"
|
|
class="modal-content"
|
|
>
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<span :class="`fs-${getMediaRank(modalMedia)}`">#{{ getMediaRank(modalMedia) }}</span>
|
|
<a
|
|
:title="
|
|
__('Voir le profile de @:username sur Instagram', {
|
|
username: modalMedia.username,
|
|
})
|
|
"
|
|
data-bs-toggle="tooltip"
|
|
:href="`https://www.instagram.com/${modalMedia.username}`"
|
|
target="_blank"
|
|
> <strong>@{{ modalMedia.username }}</strong></a>
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
data-bs-dismiss="modal"
|
|
:aria-label="__('Fermer la fenêtre')"
|
|
/>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
<div class="col-12 col-lg-6">
|
|
<div
|
|
id="carousel-modal-ig-media"
|
|
class="carousel slide"
|
|
data-bs-ride="carousel"
|
|
>
|
|
<div class="carousel-indicators">
|
|
<button
|
|
v-for="(
|
|
mediaPictureUrl, mediaPictureIndex
|
|
) of mediaPictures(modalMedia)"
|
|
:key="mediaPictureIndex"
|
|
type="button"
|
|
data-bs-target="#carousel-modal-ig-media"
|
|
:data-bs-slide-to="mediaPictureIndex"
|
|
:class="`${mediaPictureIndex === 0 ? 'active' : ''}`"
|
|
:aria-current="mediaPictureIndex === 0 ? true : false"
|
|
/>
|
|
</div>
|
|
<div class="carousel-inner">
|
|
<div
|
|
:class="`carousel-item ${
|
|
mediaPictureIndex === 0 ? 'active' : ''
|
|
}`"
|
|
v-for="(
|
|
mediaPictureUrl, mediaPictureIndex
|
|
) of mediaPictures(modalMedia)"
|
|
:key="mediaPictureIndex"
|
|
>
|
|
<img
|
|
class="d-block w-100"
|
|
:src="mediaPictureUrl"
|
|
>
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="carousel-control-prev"
|
|
type="button"
|
|
data-bs-target="#carousel-modal-ig-media"
|
|
data-bs-slide="prev"
|
|
>
|
|
<span
|
|
class="carousel-control-prev-icon"
|
|
aria-hidden="true"
|
|
/>
|
|
<span class="visually-hidden">{{ __("Précédent") }}</span>
|
|
</button>
|
|
<button
|
|
class="carousel-control-next"
|
|
type="button"
|
|
data-bs-target="#carousel-modal-ig-media"
|
|
data-bs-slide="next"
|
|
>
|
|
<span
|
|
class="carousel-control-next-icon"
|
|
aria-hidden="true"
|
|
/>
|
|
<span class="visually-hidden">{{ __("Suivant") }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<p>{{ modalMedia.caption }}</p>
|
|
<strong>
|
|
<span v-if="modalMedia.like_count">
|
|
<FontAwesomeIcon :icon="['far', 'heart']" />
|
|
<span>{{ modalMedia.like_count }}</span> </span>
|
|
<span>
|
|
<FontAwesomeIcon :icon="['far', 'message']" />
|
|
<span>{{ modalMedia.comments_count }}</span> </span>
|
|
<span>{{
|
|
new Date(
|
|
Date.parse(modalMedia.timestamp)
|
|
).toLocaleDateString()
|
|
}}</span>
|
|
</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
data-bs-dismiss="modal"
|
|
>
|
|
{{ __("Fermer") }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { defineComponent } from "vue";
|
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
|
import asset from "../../modules/asset";
|
|
import trans from "../../modules/trans";
|
|
import route from "../../modules/route";
|
|
import toast from "../../modules/toast";
|
|
import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages";
|
|
import bsTooltips from "../../modules/bsTooltips";
|
|
|
|
export default defineComponent({
|
|
name: "ParticipationListComponent",
|
|
mixins: [asset, trans, route, toast, parseLaravelErrorMessages, bsTooltips],
|
|
components: { FontAwesomeIcon },
|
|
data(): {
|
|
ajaxLoading: boolean;
|
|
disabled: boolean;
|
|
name: string;
|
|
mediaPictureList: Record<number, string[]>;
|
|
mediaList: InstaMediaList;
|
|
sMediaList: InstaMediaList;
|
|
selectedMediaList: InstaMediaList;
|
|
modalMedia: null | InstaMedia;
|
|
searchText: string;
|
|
} {
|
|
return {
|
|
ajaxLoading: false,
|
|
disabled: false,
|
|
name: "ig_media_list",
|
|
mediaPictureList: {},
|
|
mediaList: [],
|
|
sMediaList: [],
|
|
selectedMediaList: [],
|
|
modalMedia: null,
|
|
searchText: "",
|
|
};
|
|
},
|
|
mounted() {
|
|
if (window.vueDebug) {
|
|
console.log("Participation List Component mounted.");
|
|
}
|
|
const json = String(this.$attrs.json ?? "{}"),
|
|
data = JSON.parse(json);
|
|
this.mediaList = data.mediaList ?? [];
|
|
this.mediaList.forEach((media) => {
|
|
this.fetchMediaPicture(media);
|
|
});
|
|
this.selectedMediaList = data.value ?? [];
|
|
this.sMediaList = this.mediaList;
|
|
this.initTooltips();
|
|
this.fetchMediaList();
|
|
},
|
|
watch: {
|
|
searchText(value: string): void {
|
|
this.filterList(value);
|
|
},
|
|
},
|
|
methods: {
|
|
scrollTopBody() {
|
|
const element = document.getElementById(
|
|
"ig-media-cards-search"
|
|
) as HTMLLabelElement,
|
|
elementPosition = element.getBoundingClientRect().top,
|
|
offsetPosition = window.pageYOffset + elementPosition - 80;
|
|
scrollTo({
|
|
top: offsetPosition,
|
|
behavior: "smooth",
|
|
});
|
|
},
|
|
initCarousel(model: InstaMedia): void {
|
|
this.modalMedia = model;
|
|
const divCarousel = document.getElementById(
|
|
"carousel-modal-ig-media"
|
|
) as HTMLDivElement;
|
|
window.bootstrap.Carousel.getInstance(divCarousel)?.to(0);
|
|
},
|
|
fetchMediaList(force: boolean = false): void {
|
|
const topMediaList = this.route("bo.top-media-list.fetch", {
|
|
REFRESH: Number(force),
|
|
});
|
|
if (!topMediaList) {
|
|
throw Error("Missing route 'bo.top-media-list.fetch'");
|
|
}
|
|
if (!this.ajaxLoading) {
|
|
this.ajaxLoading = true;
|
|
}
|
|
window.axios
|
|
.get(topMediaList)
|
|
.then((response) => {
|
|
if (!response.data.ready) {
|
|
setTimeout(() => this.fetchMediaList(), 5000);
|
|
return;
|
|
}
|
|
this.ajaxLoading = false;
|
|
this.mediaList = response.data.data as InstaMediaList;
|
|
this.sMediaList = Object.assign([], this.mediaList);
|
|
setTimeout(
|
|
() =>
|
|
this.notify(this.__("Les posts Instagram ont été rafraichis.")),
|
|
1500
|
|
);
|
|
})
|
|
.catch((e: any) => {
|
|
this.ajaxLoading = false;
|
|
this.ajaxErrorHandler(e, this.__("Une erreur est survenue."));
|
|
})
|
|
.then(() => {
|
|
this.initTooltips();
|
|
this.sMediaList.forEach((media) => this.fetchMediaPicture(media));
|
|
});
|
|
},
|
|
fetchMediaPicture(model: InstaMedia): void {
|
|
const igHash = model.permalink.replace(
|
|
"https://www.instagram.com/p/",
|
|
""
|
|
);
|
|
const mediaPicture = this.route("bo.top-media-list.picture", {
|
|
IGMEDIAHASH: igHash.substring(0, igHash.length - 1),
|
|
});
|
|
if (!mediaPicture) {
|
|
throw Error("Missing route 'bo.top-media-list.picture'");
|
|
}
|
|
window.axios
|
|
.get(mediaPicture)
|
|
.then((response) => {
|
|
this.mediaPictureList[model.insta_id] = response.data as string[];
|
|
})
|
|
.catch((e: any) => {
|
|
this.ajaxErrorHandler(e, this.__("Une erreur est survenue."));
|
|
})
|
|
.then(() => this.initTooltips());
|
|
},
|
|
saveMediaList(): void {
|
|
const mediaIndex = this.route("bo.top-media-list.index");
|
|
if (!mediaIndex) {
|
|
throw Error("Missing route 'bo.top-media-list.index'");
|
|
}
|
|
const mediaSaveList = this.route("bo.top-media-list.save");
|
|
if (!mediaSaveList) {
|
|
throw Error("Missing route 'bo.top-media-list.save'");
|
|
}
|
|
this.ajaxLoading = true;
|
|
this.scrollTopBody();
|
|
window.axios
|
|
.post(mediaSaveList)
|
|
.then(() => {
|
|
this.ajaxLoading = false;
|
|
this.notify(this.__("La liste est enregistrée"));
|
|
this.notify(this.__("Redirection.."));
|
|
setTimeout(() => (window.location.href = mediaIndex), 2500);
|
|
})
|
|
.catch((e: any) => {
|
|
this.ajaxLoading = false;
|
|
this.ajaxErrorHandler(e, this.__("Une erreur est survenue."));
|
|
})
|
|
.then(() => this.initTooltips());
|
|
},
|
|
excludeMedia(model: InstaMedia): void {
|
|
const mediaExclude = this.route("bo.top-media-list.exclude");
|
|
if (!mediaExclude) {
|
|
throw Error("Missing route 'bo.top-media-list.exclude'");
|
|
}
|
|
this.ajaxLoading = true;
|
|
this.scrollTopBody();
|
|
window.axios
|
|
.post(mediaExclude, {
|
|
insta_id: model.insta_id,
|
|
})
|
|
.then(() => {
|
|
this.ajaxLoading = false;
|
|
this.notify(
|
|
this.__(`Le post ${model.permalink} est sur la liste d'exclusion.`)
|
|
);
|
|
this.notify(this.__("Rafraichissement de la liste.."));
|
|
this.fetchMediaList(true);
|
|
})
|
|
.catch((e: any) => {
|
|
this.ajaxLoading = false;
|
|
this.ajaxErrorHandler(e, this.__("Une erreur est survenue."));
|
|
})
|
|
.then(() => this.initTooltips());
|
|
},
|
|
excludeUser(username: string): void {
|
|
const mediaExclude = this.route("bo.top-media-list.exclude");
|
|
if (!mediaExclude) {
|
|
throw Error("Missing route 'bo.top-media-list.exclude'");
|
|
}
|
|
this.ajaxLoading = true;
|
|
this.scrollTopBody();
|
|
window.axios
|
|
.post(mediaExclude, {
|
|
username: username,
|
|
})
|
|
.then(() => {
|
|
this.ajaxLoading = false;
|
|
this.notify(
|
|
this.__(`L'utilisateur @${username} est sur la liste d'exclusion.`)
|
|
);
|
|
this.notify(this.__("Rafraichissement de la liste.."));
|
|
this.fetchMediaList(true);
|
|
})
|
|
.catch((e: any) => {
|
|
this.ajaxLoading = false;
|
|
this.ajaxErrorHandler(e, this.__("Une erreur est survenue."));
|
|
})
|
|
.then(() => this.initTooltips());
|
|
},
|
|
mediaFirstPicture(model: InstaMedia): null | string {
|
|
const pictures = this.mediaPictureList[model.insta_id] ?? [];
|
|
return pictures[0] ?? null;
|
|
},
|
|
mediaPictures(model: InstaMedia): string[] {
|
|
return this.mediaPictureList[model.insta_id] ?? [];
|
|
},
|
|
getMediaRank(model: InstaMedia): number {
|
|
return this.mediaList.indexOf(model) + 1;
|
|
},
|
|
ajaxErrorHandler(e: any, defaultMessage: string | null = null): void {
|
|
let message =
|
|
defaultMessage ?? this.__("Erreur de la récupération du panier");
|
|
if (e.response.status === 422) {
|
|
message =
|
|
this.parseValidationErrors(e.response?.data?.errors ?? {}) || message;
|
|
if (window.vueDebug) {
|
|
console.warn(e.response.data.errors);
|
|
}
|
|
} else if (e.response.status === 419) {
|
|
// * CSRF TOKEN MISMATCH (On ne pourrais plus faire d'appels AJAX sans recharger la page)
|
|
this.errorNotify(
|
|
this.__("Votre session a expiré, la page va être rechargée"),
|
|
2500
|
|
);
|
|
setTimeout(() => window.location.reload(), 2000);
|
|
} else if (e.response.status === 403) {
|
|
// * Access denied
|
|
this.errorNotify(
|
|
this.__("Vous n'êtes pas autorisé a effectuer cette action"),
|
|
2500
|
|
);
|
|
setTimeout(() => window.location.reload(), 2000);
|
|
} else {
|
|
if (window.vueDebug) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
this.errorNotify(message);
|
|
},
|
|
isChecked(model: InstaMedia): boolean {
|
|
return this.selectedMediaList.find(
|
|
(igModel) =>
|
|
igModel.insta_id === model.insta_id && model.insta_id !== null
|
|
)
|
|
? true
|
|
: false;
|
|
},
|
|
str_limit(value: string, size: number): string {
|
|
if (!value) return "";
|
|
value = value.toString();
|
|
if (value.length <= size) {
|
|
return value;
|
|
}
|
|
return value.substr(0, size) + "...";
|
|
},
|
|
filterList(search: string): void {
|
|
this.sMediaList = Object.assign([], this.mediaList);
|
|
const results = [];
|
|
|
|
if (!search.length) {
|
|
return;
|
|
}
|
|
search = search.toLowerCase();
|
|
for (const igMedia of this.mediaList) {
|
|
const igHash = igMedia.permalink.replace(
|
|
"https://www.instagram.com/p/",
|
|
""
|
|
);
|
|
const searchText = `${igMedia.username} ${igHash.substring(
|
|
0,
|
|
igHash.length - 1
|
|
)}`;
|
|
// * Doing smart search using contains, fuzzy and distance
|
|
let fuzzy =
|
|
this.stringSimilarity(searchText.toLowerCase(), search) >= 0.4 ||
|
|
this.fuzzysearch(searchText.toLowerCase(), search),
|
|
contains = searchText.toLowerCase().indexOf(search) !== -1;
|
|
|
|
for (const productWord of searchText.toLowerCase().split(" ")) {
|
|
for (const keyword of search.split(" ")) {
|
|
fuzzy =
|
|
fuzzy ||
|
|
this.stringSimilarity(productWord, keyword) >= 0.4 ||
|
|
this.fuzzysearch(productWord, keyword);
|
|
}
|
|
}
|
|
|
|
if (fuzzy || contains) {
|
|
results.push(igMedia);
|
|
}
|
|
}
|
|
this.sMediaList = results;
|
|
},
|
|
fuzzysearch(needle: string, haystack: string): boolean {
|
|
let tlen = haystack.length,
|
|
qlen = needle.length;
|
|
if (qlen > tlen) {
|
|
return false;
|
|
}
|
|
if (qlen === tlen) {
|
|
return needle === haystack;
|
|
}
|
|
outer: for (let i = 0, j = 0; i < qlen; i++) {
|
|
let nch = needle.charCodeAt(i);
|
|
while (j < tlen) {
|
|
if (haystack.charCodeAt(j++) === nch) {
|
|
continue outer;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
/**
|
|
* https://stackoverflow.com/questions/23305000/javascript-fuzzy-search-that-makes-sense
|
|
* Compares the similarity between two strings using an n-gram comparison method.
|
|
* The grams default to length 2.
|
|
* @param str1 The first string to compare.
|
|
* @param str2 The second string to compare.
|
|
* @param gramSize The size of the grams. Defaults to length 2.
|
|
*/
|
|
stringSimilarity(str1: string, str2: string, gramSize: number = 2): number {
|
|
const getNGrams = (s: string, len: number) => {
|
|
s = " ".repeat(len - 1) + s.toLowerCase() + " ".repeat(len - 1);
|
|
const v: string[] = new Array(s.length - len + 1);
|
|
for (let i = 0; i < v.length; i++) {
|
|
v[i] = s.slice(i, i + len);
|
|
}
|
|
return v;
|
|
};
|
|
|
|
if (!str1?.length || !str2?.length) {
|
|
return 0.0;
|
|
}
|
|
|
|
//Order the strings by length so the order they're passed in doesn't matter
|
|
//and so the smaller string's ngrams are always the ones in the set
|
|
const s1 = str1.length < str2.length ? str1 : str2,
|
|
s2 = str1.length < str2.length ? str2 : str1,
|
|
pairs1 = getNGrams(s1, gramSize),
|
|
pairs2 = getNGrams(s2, gramSize),
|
|
set = new Set(pairs1),
|
|
total = pairs2.length;
|
|
let hits = 0;
|
|
for (const item of pairs2) {
|
|
if (set.delete(item)) {
|
|
hits++;
|
|
}
|
|
}
|
|
return hits / total;
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@use "sass:math";
|
|
.ig-media-cards-vue {
|
|
$ig_card_height: 33rem;
|
|
$ig_card_width: 20rem;
|
|
.ig-media-cards {
|
|
justify-content: center;
|
|
}
|
|
.ig-media {
|
|
width: $ig_card_width;
|
|
height: $ig_card_height;
|
|
}
|
|
.ig-picture-mini {
|
|
max-height: math.div(17rem, $ig_card_width) * 17rem;
|
|
object-fit: contain;
|
|
}
|
|
.ig-picture-big {
|
|
max-height: 28rem;
|
|
max-width: 50%;
|
|
object-fit: contain;
|
|
}
|
|
.carousel-control-prev-icon,
|
|
.carousel-control-next-icon {
|
|
background-color: #000;
|
|
border-radius: 0.2rem;
|
|
border-top-left-radius: 0.2rem;
|
|
border-top-right-radius: 0.2rem;
|
|
border-bottom-right-radius: 0.2rem;
|
|
border-bottom-left-radius: 0.2rem;
|
|
padding: 1.3rem;
|
|
}
|
|
}
|
|
</style>
|