Create favorite store with localstorage

This commit is contained in:
pheralb
2025-08-26 11:03:57 +01:00
parent cf3918376f
commit da19647abf
4 changed files with 203 additions and 100 deletions
+40
View File
@@ -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>
+12 -6
View File
@@ -19,6 +19,8 @@
// Components:
import CopySvg from "@/components/copySvg.svelte";
import DownloadSvg from "@/components/downloadSvg.svelte";
import Heart from "@lucide/svelte/icons/heart";
import AddToFavorite from "./addToFavorite.svelte";
// Props:
interface Props {
@@ -37,16 +39,20 @@
let maxVisibleCategories = 1;
// 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>
<div
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",
"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 -->
{#if wordmarkSvg == true && svgInfo.wordmark !== undefined}
<img
@@ -99,7 +105,7 @@
{#each svgInfo.category.slice(0, maxVisibleCategories) as c, index}
<a
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`}
>
{c}
@@ -112,7 +118,7 @@
onOpenChange={(isOpen) => (moreTagsOptions = isOpen)}
>
<Popover.Trigger
class={badgeVariants({ variant: "outline" })}
class={badgeVariants({ variant: "outline", class: "font-mono" })}
title="More Tags"
>
{#if moreTagsOptions}
@@ -138,7 +144,7 @@
{:else}
<a
href={`/directory/${svgInfo.category.toLowerCase()}`}
class={badgeVariants({ variant: "outline" })}
class={badgeVariants({ variant: "outline", class: "font-mono" })}
>
{svgInfo.category}
</a>
@@ -146,7 +152,7 @@
</div>
</div>
<!-- Actions -->
<div class="flex items-center space-x-1">
<div class="flex items-center space-x-0.5">
{#if wordmarkSvg && svgInfo.wordmark !== undefined}
<CopySvg
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>
+151
View File
@@ -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;