mirror of
https://github.com/pheralb/svgl.git
synced 2025-12-29 08:01:36 +08:00
✨ Create favorite store with localstorage
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { iSVG } from "@/types/svg";
|
||||||
|
import favoritesStore from "@/stores/favorites.store";
|
||||||
|
import HeartIcon from "@lucide/svelte/icons/heart";
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
svg: iSVG;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { svg }: Props = $props();
|
||||||
|
|
||||||
|
let favorites = $derived($favoritesStore);
|
||||||
|
let isFavorite = $derived(favoritesStore.isFavorite(svg, favorites));
|
||||||
|
|
||||||
|
const toggleFavorite = () => {
|
||||||
|
favoritesStore.toggleFavorite(svg);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class={cn(
|
||||||
|
"cursor-pointer transition-colors hover:animate-pulse",
|
||||||
|
"text-neutral-500 hover:text-red-700 dark:text-neutral-400 dark:hover:text-red-400",
|
||||||
|
isFavorite && "text-red-500",
|
||||||
|
)}
|
||||||
|
onclick={toggleFavorite}
|
||||||
|
title={isFavorite
|
||||||
|
? `Delete ${svg.title} from favorites`
|
||||||
|
: `Add ${svg.title} to favorites`}
|
||||||
|
aria-label={isFavorite
|
||||||
|
? `Delete ${svg.title} from favorites`
|
||||||
|
: `Add ${svg.title} to favorites`}
|
||||||
|
>
|
||||||
|
<HeartIcon
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.8}
|
||||||
|
class={cn(isFavorite && "fill-red-500")}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
// Components:
|
// Components:
|
||||||
import CopySvg from "@/components/copySvg.svelte";
|
import CopySvg from "@/components/copySvg.svelte";
|
||||||
import DownloadSvg from "@/components/downloadSvg.svelte";
|
import DownloadSvg from "@/components/downloadSvg.svelte";
|
||||||
|
import Heart from "@lucide/svelte/icons/heart";
|
||||||
|
import AddToFavorite from "./addToFavorite.svelte";
|
||||||
|
|
||||||
// Props:
|
// Props:
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -37,16 +39,20 @@
|
|||||||
let maxVisibleCategories = 1;
|
let maxVisibleCategories = 1;
|
||||||
|
|
||||||
// Global Styles:
|
// Global Styles:
|
||||||
const globalImageStyles = "mb-4 mt-2 h-10 select-none pointer-events-none";
|
const globalImageStyles = "mb-4 mt-1.5 h-10 select-none pointer-events-none";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
"group flex flex-col items-center justify-center p-4",
|
"group flex flex-col items-center justify-center px-3.5 py-3",
|
||||||
"rounded-md border border-neutral-200 dark:border-neutral-800",
|
"rounded-md border border-neutral-200 dark:border-neutral-800",
|
||||||
"transition-colors duration-100 hover:bg-neutral-100/80 dark:hover:bg-neutral-800/20",
|
"transition-colors duration-100 hover:bg-neutral-100/80 dark:hover:bg-neutral-800/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<!-- Image Options -->
|
||||||
|
<div class="flex w-full items-center justify-end space-x-0.5">
|
||||||
|
<AddToFavorite svg={svgInfo} />
|
||||||
|
</div>
|
||||||
<!-- Image -->
|
<!-- Image -->
|
||||||
{#if wordmarkSvg == true && svgInfo.wordmark !== undefined}
|
{#if wordmarkSvg == true && svgInfo.wordmark !== undefined}
|
||||||
<img
|
<img
|
||||||
@@ -99,7 +105,7 @@
|
|||||||
{#each svgInfo.category.slice(0, maxVisibleCategories) as c, index}
|
{#each svgInfo.category.slice(0, maxVisibleCategories) as c, index}
|
||||||
<a
|
<a
|
||||||
href={`/directory/${c.toLowerCase()}`}
|
href={`/directory/${c.toLowerCase()}`}
|
||||||
class={badgeVariants({ variant: "outline" })}
|
class={badgeVariants({ variant: "outline", class: "font-mono" })}
|
||||||
title={`This icon is part of the ${svgInfo.category} category`}
|
title={`This icon is part of the ${svgInfo.category} category`}
|
||||||
>
|
>
|
||||||
{c}
|
{c}
|
||||||
@@ -112,7 +118,7 @@
|
|||||||
onOpenChange={(isOpen) => (moreTagsOptions = isOpen)}
|
onOpenChange={(isOpen) => (moreTagsOptions = isOpen)}
|
||||||
>
|
>
|
||||||
<Popover.Trigger
|
<Popover.Trigger
|
||||||
class={badgeVariants({ variant: "outline" })}
|
class={badgeVariants({ variant: "outline", class: "font-mono" })}
|
||||||
title="More Tags"
|
title="More Tags"
|
||||||
>
|
>
|
||||||
{#if moreTagsOptions}
|
{#if moreTagsOptions}
|
||||||
@@ -138,7 +144,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
href={`/directory/${svgInfo.category.toLowerCase()}`}
|
href={`/directory/${svgInfo.category.toLowerCase()}`}
|
||||||
class={badgeVariants({ variant: "outline" })}
|
class={badgeVariants({ variant: "outline", class: "font-mono" })}
|
||||||
>
|
>
|
||||||
{svgInfo.category}
|
{svgInfo.category}
|
||||||
</a>
|
</a>
|
||||||
@@ -146,7 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-0.5">
|
||||||
{#if wordmarkSvg && svgInfo.wordmark !== undefined}
|
{#if wordmarkSvg && svgInfo.wordmark !== undefined}
|
||||||
<CopySvg
|
<CopySvg
|
||||||
size={iconSize}
|
size={iconSize}
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
<script>
|
|
||||||
/**
|
|
||||||
* @typedef {Object} Props
|
|
||||||
* @property {string} [color]
|
|
||||||
* @property {number} [size]
|
|
||||||
* @property {number} [strokeWidth]
|
|
||||||
* @property {boolean} [isHovered]
|
|
||||||
* @property {string} [class]
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {Props} */
|
|
||||||
let {
|
|
||||||
color = "currentColor",
|
|
||||||
size = 24,
|
|
||||||
strokeWidth = 2,
|
|
||||||
isHovered = false,
|
|
||||||
class: className = "",
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
function handleMouseEnter() {
|
|
||||||
isHovered = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
isHovered = false;
|
|
||||||
}, 1200);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={className}
|
|
||||||
aria-label="heart"
|
|
||||||
role="img"
|
|
||||||
onmouseenter={handleMouseEnter}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke={color}
|
|
||||||
stroke-width={strokeWidth}
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="heart-icon"
|
|
||||||
class:animate={isHovered}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"
|
|
||||||
class="heart-path"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
div {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.heart-icon {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart-path {
|
|
||||||
transform-origin: center;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart-icon.animate .heart-path {
|
|
||||||
animation: heartBeat 1.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes heartBeat {
|
|
||||||
0% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
16.67% {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
33.33% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
66.67% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
83.33% {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import type { iSVG } from "@/types/svg";
|
||||||
|
import { svgs } from "@/data/svgs";
|
||||||
|
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
|
const localStorageKey = "svgl_favorites";
|
||||||
|
|
||||||
|
function createFavoritesStore() {
|
||||||
|
// Check if the favorites exist in the SVGs array:
|
||||||
|
const validateFavorites = (favorites: iSVG[]): iSVG[] => {
|
||||||
|
return favorites.filter((favorite) => {
|
||||||
|
const existsInSvgs = svgs.some((svg) => {
|
||||||
|
return (
|
||||||
|
svg.title === favorite.title &&
|
||||||
|
JSON.stringify(svg.route) === JSON.stringify(favorite.route)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existsInSvgs) {
|
||||||
|
console.warn(
|
||||||
|
`🗑️ Favorito eliminado: "${favorite.title}" ya no existe en la colección de SVGs`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return existsInSvgs;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFavorites = (): iSVG[] => {
|
||||||
|
if (browser) {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(localStorageKey);
|
||||||
|
if (stored) {
|
||||||
|
const storedFavorites: iSVG[] = JSON.parse(stored);
|
||||||
|
const validatedFavorites = validateFavorites(storedFavorites);
|
||||||
|
if (validatedFavorites.length !== storedFavorites.length) {
|
||||||
|
localStorage.setItem(
|
||||||
|
localStorageKey,
|
||||||
|
JSON.stringify(validatedFavorites),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return validatedFavorites;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ stores/favorites - Error loading favorites:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveFavorites = (favorites: iSVG[]) => {
|
||||||
|
if (browser) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(localStorageKey, JSON.stringify(favorites));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ stores/favorites - Error saving favorites:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable<iSVG[]>(loadFavorites());
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
|
||||||
|
// Add SVG to favorites:
|
||||||
|
addToFavorites: (item: iSVG) =>
|
||||||
|
update((favorites) => {
|
||||||
|
const exists = favorites.some((fav) =>
|
||||||
|
typeof item === "object" && item.id
|
||||||
|
? fav.id === item.id
|
||||||
|
: fav === item,
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
const newFavorites = [...favorites, item];
|
||||||
|
saveFavorites(newFavorites);
|
||||||
|
return newFavorites;
|
||||||
|
}
|
||||||
|
return favorites;
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Delete SVG from favorites:
|
||||||
|
removeFromFavorites: (item: iSVG) =>
|
||||||
|
update((favorites) => {
|
||||||
|
const newFavorites = favorites.filter((fav) =>
|
||||||
|
typeof item === "object" && item.id
|
||||||
|
? fav.id !== item.id
|
||||||
|
: fav !== item,
|
||||||
|
);
|
||||||
|
saveFavorites(newFavorites);
|
||||||
|
return newFavorites;
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Toggle (add/remove) SVG from favorites:
|
||||||
|
toggleFavorite: (item: iSVG) =>
|
||||||
|
update((favorites) => {
|
||||||
|
const exists = favorites.some((fav) =>
|
||||||
|
typeof item === "object" && item.id
|
||||||
|
? fav.id === item.id
|
||||||
|
: fav === item,
|
||||||
|
);
|
||||||
|
|
||||||
|
let newFavorites;
|
||||||
|
if (exists) {
|
||||||
|
newFavorites = favorites.filter((fav) =>
|
||||||
|
typeof item === "object" && item.id
|
||||||
|
? fav.id !== item.id
|
||||||
|
: fav !== item,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
newFavorites = [...favorites, item];
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFavorites(newFavorites);
|
||||||
|
return newFavorites;
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Check if SVG is in favorites:
|
||||||
|
isFavorite: (item: iSVG, currentFavorites: iSVG[]) => {
|
||||||
|
return currentFavorites.some((fav) =>
|
||||||
|
typeof item === "object" && item.id ? fav.id === item.id : fav === item,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete all favorites:
|
||||||
|
clearFavorites: () => {
|
||||||
|
set([]);
|
||||||
|
saveFavorites([]);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get count of favorites:
|
||||||
|
getCount: (currentFavorites: iSVG[]) => currentFavorites.length,
|
||||||
|
|
||||||
|
validateAndCleanup: () => {
|
||||||
|
update((favorites) => {
|
||||||
|
const validatedFavorites = validateFavorites(favorites);
|
||||||
|
if (validatedFavorites.length !== favorites.length) {
|
||||||
|
saveFavorites(validatedFavorites);
|
||||||
|
}
|
||||||
|
return validatedFavorites;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const favoritesStore = createFavoritesStore();
|
||||||
|
|
||||||
|
export default favoritesStore;
|
||||||
Reference in New Issue
Block a user