Skip to content

Commit dca1d3e

Browse files
committed
Implemented quick tips
1 parent 1e1b87f commit dca1d3e

File tree

2 files changed

+348
-0
lines changed

2 files changed

+348
-0
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
<script lang="ts">
2+
import { onMount } from 'svelte';
3+
import { browser } from '$app/environment';
4+
import Icon from '$lib/components/global/Icon.svelte';
5+
import { tooltip } from '$lib/actions/tooltip';
6+
7+
interface Tip {
8+
icon: string;
9+
title: string;
10+
description: string;
11+
shortcut?: string;
12+
}
13+
14+
const tips: Tip[] = [
15+
{
16+
icon: 'settings',
17+
title: 'Customize the app in the settings',
18+
description: 'Choose your homepage layout, nav links, theme and more',
19+
shortcut: 'Ctrl + ,',
20+
},
21+
{
22+
icon: 'bookmarks',
23+
title: 'Bookmark tools for easy access and offline use',
24+
description: 'Just right-click on any tool to bookmark or edit it',
25+
},
26+
{
27+
icon: 'search',
28+
title: 'Use Ctrl + K to quickly search all tools',
29+
description: 'Or, try Ctrl + / to view all shortcuts',
30+
shortcut: 'Ctrl + K',
31+
},
32+
];
33+
34+
let visible = $state(false);
35+
let currentTipIndex = $state(0);
36+
let mounted = $state(false);
37+
38+
const STORAGE_KEY = 'networking-toolbox-tips-dismissed';
39+
const TOOL_USAGE_KEY = 'networking-toolbox-tool-usage';
40+
41+
function shouldShowTips(): boolean {
42+
if (!browser) return false;
43+
44+
try {
45+
// Check if tips were dismissed
46+
const dismissed = localStorage.getItem(STORAGE_KEY);
47+
if (dismissed === 'true') return false;
48+
49+
// Check tool usage count
50+
const toolUsageStr = localStorage.getItem(TOOL_USAGE_KEY);
51+
if (toolUsageStr) {
52+
const toolUsage = JSON.parse(toolUsageStr);
53+
const visitCount = Object.keys(toolUsage).length;
54+
if (visitCount >= 3) return false;
55+
}
56+
57+
return true;
58+
} catch {
59+
return true;
60+
}
61+
}
62+
63+
function dismissTips() {
64+
if (!browser) return;
65+
try {
66+
localStorage.setItem(STORAGE_KEY, 'true');
67+
} catch {
68+
// Ignore localStorage errors
69+
}
70+
visible = false;
71+
}
72+
73+
function nextTip() {
74+
currentTipIndex = (currentTipIndex + 1) % tips.length;
75+
}
76+
77+
function previousTip() {
78+
currentTipIndex = (currentTipIndex - 1 + tips.length) % tips.length;
79+
}
80+
81+
onMount(() => {
82+
mounted = true;
83+
if (shouldShowTips()) {
84+
// Load last viewed tip index
85+
try {
86+
const lastIndex = localStorage.getItem('networking-toolbox-tip-index');
87+
if (lastIndex) {
88+
currentTipIndex = parseInt(lastIndex, 10) % tips.length;
89+
}
90+
} catch {
91+
// Ignore
92+
}
93+
94+
// Show tips after a brief delay for smooth entrance
95+
setTimeout(() => {
96+
visible = true;
97+
}, 800);
98+
}
99+
});
100+
101+
// Save current tip index when it changes
102+
$effect(() => {
103+
if (browser && mounted) {
104+
try {
105+
localStorage.setItem('networking-toolbox-tip-index', currentTipIndex.toString());
106+
} catch {
107+
// Ignore
108+
}
109+
}
110+
});
111+
112+
const currentTip = $derived(tips[currentTipIndex]);
113+
</script>
114+
115+
{#if visible}
116+
<div class="quick-tips" role="complementary" aria-label="Quick tips">
117+
<button
118+
class="close-btn"
119+
onclick={dismissTips}
120+
aria-label="Dismiss tips"
121+
use:tooltip={"Hide, and don't show tips again"}
122+
>
123+
<Icon name="x" size="sm" />
124+
</button>
125+
126+
<div class="tip-main">
127+
<div class="tip-content">
128+
<div class="tip-icon">
129+
<Icon name={currentTip.icon} size="md" />
130+
</div>
131+
132+
<div class="tip-text">
133+
<h3>Tip: {currentTip.title}</h3>
134+
<p>{currentTip.description}</p>
135+
<!-- {#if currentTip.shortcut}
136+
<div class="tip-shortcut">
137+
<kbd>{currentTip.shortcut}</kbd>
138+
</div>
139+
{/if} -->
140+
</div>
141+
</div>
142+
143+
<div class="tip-controls">
144+
<button class="nav-btn" onclick={previousTip} aria-label="Previous tip">
145+
<Icon name="arrow-left" size="sm" />
146+
</button>
147+
<button class="nav-btn" onclick={nextTip} aria-label="Next tip">
148+
<Icon name="arrow-right" size="sm" />
149+
</button>
150+
</div>
151+
</div>
152+
153+
<div class="tip-dots">
154+
{#each tips as _, index (index)}
155+
<button
156+
class="dot"
157+
class:active={index === currentTipIndex}
158+
onclick={() => (currentTipIndex = index)}
159+
aria-label="Go to tip {index + 1}"
160+
></button>
161+
{/each}
162+
</div>
163+
</div>
164+
{/if}
165+
166+
<style lang="scss">
167+
.quick-tips {
168+
position: relative;
169+
background: linear-gradient(
170+
135deg,
171+
color-mix(in srgb, var(--color-primary), transparent 92%),
172+
color-mix(in srgb, var(--color-primary), transparent 96%)
173+
);
174+
border: 1px solid color-mix(in srgb, var(--color-primary), transparent 70%);
175+
border-radius: var(--radius-lg);
176+
padding: var(--spacing-md) var(--spacing-lg);
177+
margin-bottom: var(--spacing-xl);
178+
box-shadow: var(--shadow-md);
179+
animation: slideInDown 0.5s ease-out;
180+
181+
@media (max-width: 768px) {
182+
padding: var(--spacing-sm) var(--spacing-md);
183+
margin-bottom: var(--spacing-lg);
184+
}
185+
186+
@media (max-width: 480px) {
187+
display: none;
188+
}
189+
}
190+
191+
.close-btn {
192+
position: absolute;
193+
top: var(--spacing-sm);
194+
right: var(--spacing-sm);
195+
background: transparent;
196+
border: none;
197+
color: var(--text-secondary);
198+
cursor: pointer;
199+
padding: var(--spacing-xs);
200+
border-radius: var(--radius-sm);
201+
transition: all var(--transition-fast);
202+
display: flex;
203+
align-items: center;
204+
justify-content: center;
205+
206+
&:hover {
207+
background: var(--bg-tertiary);
208+
color: var(--text-primary);
209+
}
210+
}
211+
212+
.tip-main {
213+
display: flex;
214+
align-items: flex-start;
215+
justify-content: space-between;
216+
gap: var(--spacing-md);
217+
margin-bottom: var(--spacing-xs);
218+
219+
@media (max-width: 640px) {
220+
flex-direction: column;
221+
align-items: flex-start;
222+
}
223+
}
224+
225+
.tip-content {
226+
display: flex;
227+
gap: var(--spacing-md);
228+
align-items: center;
229+
flex: 1;
230+
padding-right: var(--spacing-lg);
231+
margin-top: var(--spacing-sm);
232+
233+
@media (max-width: 640px) {
234+
gap: var(--spacing-sm);
235+
padding-right: 0;
236+
}
237+
}
238+
239+
.tip-icon {
240+
flex-shrink: 0;
241+
width: 2.5rem;
242+
height: 2.5rem;
243+
display: flex;
244+
align-items: center;
245+
justify-content: center;
246+
background: color-mix(in srgb, var(--color-primary), transparent 85%);
247+
border-radius: var(--radius-md);
248+
249+
:global(svg) {
250+
color: var(--color-primary);
251+
}
252+
253+
@media (max-width: 640px) {
254+
width: 2rem;
255+
height: 2rem;
256+
}
257+
}
258+
259+
.tip-text {
260+
flex: 1;
261+
262+
h3 {
263+
font-size: var(--font-size-md);
264+
font-weight: 600;
265+
color: var(--text-primary);
266+
margin: 0 0 var(--spacing-xs) 0;
267+
line-height: 1.3;
268+
}
269+
270+
p {
271+
font-size: var(--font-size-sm);
272+
color: var(--text-secondary);
273+
margin: 0;
274+
line-height: 1.4;
275+
}
276+
}
277+
278+
.tip-controls {
279+
display: flex;
280+
align-items: center;
281+
gap: var(--spacing-xs);
282+
flex-shrink: 0;
283+
align-self: flex-end;
284+
285+
@media (max-width: 640px) {
286+
align-self: flex-end;
287+
}
288+
289+
.nav-btn {
290+
background: transparent;
291+
border: 1px solid var(--border-primary);
292+
color: var(--text-secondary);
293+
cursor: pointer;
294+
padding: var(--spacing-xs);
295+
border-radius: var(--radius-sm);
296+
transition: all var(--transition-fast);
297+
display: flex;
298+
align-items: center;
299+
justify-content: center;
300+
301+
&:hover {
302+
background: var(--bg-tertiary);
303+
border-color: var(--border-secondary);
304+
color: var(--text-primary);
305+
}
306+
}
307+
}
308+
309+
.tip-dots {
310+
display: flex;
311+
justify-content: center;
312+
gap: var(--spacing-xs);
313+
padding-top: var(--spacing-xs);
314+
315+
.dot {
316+
width: 0.5rem;
317+
height: 0.5rem;
318+
border-radius: var(--radius-full);
319+
background: var(--border-primary);
320+
border: none;
321+
cursor: pointer;
322+
padding: 0;
323+
transition: all var(--transition-fast);
324+
325+
&:hover {
326+
background: var(--text-secondary);
327+
}
328+
329+
&.active {
330+
background: var(--color-primary);
331+
}
332+
}
333+
}
334+
335+
@keyframes slideInDown {
336+
from {
337+
opacity: 0;
338+
transform: translateY(-1rem);
339+
}
340+
to {
341+
opacity: 1;
342+
transform: translateY(0);
343+
}
344+
}
345+
</style>

src/lib/components/home/HomepageCategories.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { frequentlyUsedTools, recentlyUsedTools } from '$lib/stores/toolUsage';
88
import Icon from '$lib/components/global/Icon.svelte';
99
import { extractNavItems } from '$lib/utils/nav';
10+
import QuickTips from '$lib/components/furniture/QuickTips.svelte';
1011
1112
interface Props {
1213
toolPages: NavItem[];
@@ -115,6 +116,8 @@
115116
<!-- Search Filter -->
116117
<SearchFilter bind:filteredTools bind:searchQuery />
117118

119+
<QuickTips />
120+
118121
{#if searchQuery.trim() === ''}
119122
<div class="categories-layout">
120123
<!-- Bookmarks or Featured Section -->

0 commit comments

Comments
 (0)