Skip to content

Commit d7025c3

Browse files
committed
feat: command palette 4
1 parent 199a573 commit d7025c3

File tree

5 files changed

+147
-141
lines changed

5 files changed

+147
-141
lines changed

src/components/CommandPalette.js

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { useAnimation } from '../context/AnimationContext';
66
import { useToast } from '../hooks/useToast';
77
import { SIDEBAR_KEYS, remove as removeLocalStorageItem } from '../utils/LocalStorageManager';
88
import { version } from '../version'; // Import the version
9+
import LiveClock from './LiveClock'; // Import LiveClock
10+
import { filterItems } from '../utils/search'; // Import the search utility
911

1012
const CommandPalette = ({ isOpen, setIsOpen, openGenericModal }) => {
1113
const [searchTerm, setSearchTerm] = useState('');
@@ -17,18 +19,7 @@ const CommandPalette = ({ isOpen, setIsOpen, openGenericModal }) => {
1719
const { isAnimationEnabled, toggleAnimation } = useAnimation();
1820
const { addToast } = useToast();
1921

20-
const filteredItems = searchTerm
21-
? items.filter(item => {
22-
const lowerCaseSearchTerm = searchTerm.toLowerCase();
23-
const titleMatch = item.title.toLowerCase().includes(lowerCaseSearchTerm);
24-
const typeMatch = item.type.toLowerCase().includes(lowerCaseSearchTerm);
25-
const tagMatch = item.tags?.some(tag => tag.toLowerCase().includes(lowerCaseSearchTerm));
26-
const techMatch = item.technologies?.some(tech => tech.toLowerCase().includes(lowerCaseSearchTerm));
27-
const categoryMatch = item.category?.toLowerCase().includes(lowerCaseSearchTerm);
28-
29-
return titleMatch || typeMatch || tagMatch || techMatch || categoryMatch;
30-
})
31-
: items;
22+
const filteredItems = filterItems(items, searchTerm);
3223

3324
useEffect(() => {
3425
if (isOpen) {
@@ -131,6 +122,10 @@ const CommandPalette = ({ isOpen, setIsOpen, openGenericModal }) => {
131122
case 'herDaim':
132123
openGenericModal('Her Daim', <img src="/images/herdaim.jpg" alt="Her Daim" className="max-w-full h-auto" />);
133124
break;
125+
case 'showTime': {
126+
openGenericModal('Current Time', <LiveClock />);
127+
break;
128+
}
134129
default:
135130
break;
136131
}
@@ -247,4 +242,4 @@ const CommandPalette = ({ isOpen, setIsOpen, openGenericModal }) => {
247242
);
248243
};
249244

250-
export default CommandPalette;
245+
export default CommandPalette;

src/components/LiveClock.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React, { useState, useEffect } from 'react';
2+
3+
// AnalogClock component to render a single clock
4+
const AnalogClock = ({ time, title }) => {
5+
const getHandRotations = (date) => {
6+
const seconds = date.getSeconds();
7+
const minutes = date.getMinutes();
8+
const hours = date.getHours();
9+
10+
const secondsDeg = (seconds / 60) * 360;
11+
const minutesDeg = (minutes / 60) * 360 + (seconds / 60) * 6;
12+
const hoursDeg = (hours / 12) * 360 + (minutes / 60) * 30;
13+
14+
return { secondsDeg, minutesDeg, hoursDeg };
15+
};
16+
17+
const { secondsDeg, minutesDeg, hoursDeg } = getHandRotations(time);
18+
19+
return (
20+
<div className="flex flex-col items-center">
21+
<h3 className="text-lg font-semibold mb-2">{title}</h3>
22+
<svg width="200" height="200" viewBox="0 0 200 200">
23+
{/* Clock face */}
24+
<circle cx="100" cy="100" r="98" fill="#1F2937" stroke="#9CA3AF" strokeWidth="4" />
25+
{/* Hour markers */}
26+
{Array.from({ length: 12 }).map((_, i) => (
27+
<line
28+
key={i}
29+
x1="100"
30+
y1="10"
31+
x2="100"
32+
y2="20"
33+
stroke="#9CA3AF"
34+
strokeWidth="2"
35+
transform={`rotate(${i * 30} 100 100)`}
36+
/>
37+
))}
38+
39+
{/* Hour Hand */}
40+
<line
41+
x1="100"
42+
y1="100"
43+
x2="100"
44+
y2="50"
45+
stroke="#FBBF24"
46+
strokeWidth="6"
47+
strokeLinecap="round"
48+
style={{ transformOrigin: 'center', transform: `rotate(${hoursDeg}deg)` }}
49+
/>
50+
{/* Minute Hand */}
51+
<line
52+
x1="100"
53+
y1="100"
54+
x2="100"
55+
y2="30"
56+
stroke="#A7F3D0"
57+
strokeWidth="4"
58+
strokeLinecap="round"
59+
style={{ transformOrigin: 'center', transform: `rotate(${minutesDeg}deg)` }}
60+
/>
61+
{/* Second Hand */}
62+
<line
63+
x1="100"
64+
y1="100"
65+
x2="100"
66+
y2="20"
67+
stroke="#F87171"
68+
strokeWidth="2"
69+
strokeLinecap="round"
70+
style={{ transformOrigin: 'center', transform: `rotate(${secondsDeg}deg)` }}
71+
/>
72+
{/* Center dot */}
73+
<circle cx="100" cy="100" r="5" fill="#FBBF24" />
74+
</svg>
75+
</div>
76+
);
77+
};
78+
79+
// Main LiveClock component to manage time state
80+
const LiveClock = () => {
81+
const [now, setNow] = useState(new Date());
82+
83+
useEffect(() => {
84+
const intervalId = setInterval(() => {
85+
setNow(new Date());
86+
}, 1000); // Update every second
87+
88+
return () => clearInterval(intervalId); // Cleanup interval on component unmount
89+
}, []);
90+
91+
// Create a date object for UTC time
92+
const utcDate = new Date(
93+
now.getUTCFullYear(),
94+
now.getUTCMonth(),
95+
now.getUTCDate(),
96+
now.getUTCHours(),
97+
now.getUTCMinutes(),
98+
now.getUTCSeconds()
99+
);
100+
101+
return (
102+
<div className="flex justify-center gap-8 p-4">
103+
<AnalogClock time={now} title="Local Time" />
104+
<AnalogClock time={utcDate} title="UTC Time" />
105+
</div>
106+
);
107+
};
108+
109+
export default LiveClock;

src/components/Search.js

Lines changed: 9 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
import React, { useState, useEffect, useRef } from 'react';
22
import { Link } from 'react-router-dom';
33
import { MagnifyingGlassIcon } from '@phosphor-icons/react';
4+
import useSearchableData from '../hooks/useSearchableData';
5+
import { filterItems } from '../utils/search';
46

57
const Search = ({ isVisible }) => {
68
const [searchTerm, setSearchTerm] = useState('');
79
const [searchResults, setSearchResults] = useState([]);
810
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
9-
const [data, setData] = useState({
10-
posts: [],
11-
projects: [],
12-
logs: [],
13-
routes: [],
14-
apps: [],
15-
});
11+
const { items, isLoading } = useSearchableData();
1612
const searchRef = useRef(null);
1713
const inputRef = useRef(null);
1814

@@ -22,119 +18,16 @@ const Search = ({ isVisible }) => {
2218
}
2319
}, [isVisible]);
2420

25-
useEffect(() => {
26-
const fetchData = async () => {
27-
try {
28-
const [postsRes, projectsRes, logsRes, appsRes] = await Promise.all([
29-
fetch('/posts/posts.json'),
30-
fetch('/projects/projects.json'),
31-
fetch('/logs/logs.json'),
32-
fetch('/apps/apps.json'),
33-
]);
34-
35-
const posts = await postsRes.json();
36-
const projects = await projectsRes.json();
37-
const logs = await logsRes.json();
38-
const appsData = await appsRes.json();
39-
const allApps = Object.values(appsData).flatMap(
40-
(category) => category.apps,
41-
);
42-
43-
const allPosts = posts.flatMap((item) =>
44-
item.series
45-
? item.series.posts.map((p) => ({ ...p, series: item.title }))
46-
: item,
47-
);
48-
49-
// Manually define common routes
50-
const routes = [
51-
{ title: 'Home', slug: '/', type: 'route' },
52-
{ title: 'Blogs', slug: '/blog', type: 'route' },
53-
{ title: 'Projects', slug: '/projects', type: 'route' },
54-
{ title: 'About Me', slug: '/about', type: 'route' },
55-
{ title: 'Logs', slug: '/logs', type: 'route' },
56-
{ title: 'Settings', slug: '/settings', type: 'route' },
57-
{ title: 'Dungeons and Dragons', slug: '/stories', type: 'route' },
58-
{ title: 'Stories', slug: '/stories', type: 'route' },
59-
{ title: 'From Serfs and Frauds', slug: '/stories', type: 'route' },
60-
{ title: 'Apps', slug: '/apps', type: 'route' },
61-
{ title: 'Random', slug: '/random', type: 'route' },
62-
];
63-
64-
setData({
65-
posts: allPosts,
66-
projects: projects,
67-
logs: logs,
68-
routes: routes,
69-
apps: allApps,
70-
}); // Include routes in data
71-
} catch (error) {
72-
console.error('Failed to fetch search data:', error);
73-
}
74-
};
75-
76-
fetchData();
77-
}, []);
78-
7921
useEffect(() => {
8022
if (searchTerm) {
81-
const lowerCaseSearchTerm = searchTerm.toLowerCase();
82-
const posts = data.posts
83-
.filter(
84-
(post) =>
85-
post.title.toLowerCase().includes(lowerCaseSearchTerm) ||
86-
post.tags?.some((tag) =>
87-
tag.toLowerCase().includes(lowerCaseSearchTerm),
88-
),
89-
)
90-
.map((post) => ({ ...post, type: 'post' }));
91-
92-
const projects = data.projects
93-
.filter(
94-
(project) =>
95-
project.title.toLowerCase().includes(lowerCaseSearchTerm) ||
96-
project.technologies?.some((tech) =>
97-
tech.toLowerCase().includes(lowerCaseSearchTerm),
98-
),
99-
)
100-
.map((project) => ({ ...project, type: 'project' }));
101-
102-
const logs = data.logs
103-
.filter(
104-
(log) =>
105-
log.title.toLowerCase().includes(lowerCaseSearchTerm) ||
106-
log.category?.toLowerCase().includes(lowerCaseSearchTerm) ||
107-
log.tags?.some((tag) =>
108-
tag.toLowerCase().includes(lowerCaseSearchTerm),
109-
),
110-
)
111-
.map((log) => ({ ...log, type: 'log' }));
112-
113-
// Filter routes
114-
const routes = data.routes
115-
.filter(
116-
(route) =>
117-
route.title.toLowerCase().includes(lowerCaseSearchTerm) ||
118-
route.slug.toLowerCase().includes(lowerCaseSearchTerm),
119-
)
120-
.map((route) => ({ ...route, type: 'route' }));
121-
122-
// Filter apps
123-
const apps = data.apps
124-
.filter(
125-
(app) =>
126-
app.title.toLowerCase().includes(lowerCaseSearchTerm) ||
127-
app.slug.toLowerCase().includes(lowerCaseSearchTerm),
128-
)
129-
.map((app) => ({ ...app, type: 'app' }));
130-
131-
setSearchResults([...posts, ...projects, ...logs, ...routes, ...apps]); // Include routes in search results
23+
const results = filterItems(items, searchTerm);
24+
setSearchResults(results);
13225
setIsDropdownOpen(true);
13326
} else {
13427
setSearchResults([]);
13528
setIsDropdownOpen(false);
13629
}
137-
}, [searchTerm, data]);
30+
}, [searchTerm, items]);
13831

13932
useEffect(() => {
14033
const handleClickOutside = (event) => {
@@ -150,20 +43,7 @@ const Search = ({ isVisible }) => {
15043
}, []);
15144

15245
const getResultLink = (result) => {
153-
switch (result.type) {
154-
case 'post':
155-
return `/blog/${result.slug}`;
156-
case 'project':
157-
return `/projects/${result.slug}`;
158-
case 'log':
159-
return `/logs/${result.slug}`;
160-
case 'route': // Handle routes
161-
return result.slug;
162-
case 'app': // Handle apps
163-
return `${result.to}`;
164-
default:
165-
return '/';
166-
}
46+
return result.path || '/';
16747
};
16848

16949
if (!isVisible) {
@@ -182,11 +62,12 @@ const Search = ({ isVisible }) => {
18262
<input
18363
ref={inputRef}
18464
type="text"
185-
placeholder="Search..."
65+
placeholder={isLoading ? "Loading..." : "Search..."}
18666
value={searchTerm}
18767
onChange={(e) => setSearchTerm(e.target.value)}
18868
onFocus={() => setIsDropdownOpen(true)}
18969
className="bg-gray-800 text-white w-full py-2 px-4 pl-10 focus:outline-none focus:ring-2 focus:ring-primary-400 rounded-md"
70+
disabled={isLoading}
19071
/>
19172
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
19273
{isDropdownOpen && searchResults.length > 0 && (

src/hooks/useSearchableData.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const useSearchableData = () => {
7676
{ title: 'Go to Latest Post', type: 'command', commandId: 'latestPost' },
7777
{ title: 'Go to Latest Log', type: 'command', commandId: 'latestLog' },
7878
{ title: 'Her Daim', type: 'command', commandId: 'herDaim' },
79+
{ title: 'Show Current Time', type: 'command', commandId: 'showTime' },
7980
];
8081

8182
setItems([...staticRoutes, ...customCommands, ...allPosts, ...allProjects, ...allLogs, ...allApps]);

src/utils/search.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export const filterItems = (items, searchTerm) => {
2+
if (!searchTerm) {
3+
return items;
4+
}
5+
6+
const lowerCaseSearchTerm = searchTerm.toLowerCase();
7+
const searchWords = lowerCaseSearchTerm.split(' ').filter(w => w);
8+
9+
return items.filter(item => {
10+
const targetText = [
11+
item.title?.toLowerCase(),
12+
item.type?.toLowerCase(),
13+
...(item.tags || []).map(tag => tag.toLowerCase()),
14+
...(item.technologies || []).map(tech => tech.toLowerCase()),
15+
item.category?.toLowerCase(),
16+
].filter(Boolean).join(' ');
17+
18+
return searchWords.every(word => targetText.includes(word));
19+
});
20+
};

0 commit comments

Comments
 (0)