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:
|
||||
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>
|
||||
@@ -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