Skip to content

Commit a4f0387

Browse files
Merge pull request #2 from mohammadumar-dev/develop
Added Components
2 parents d5b66e2 + a22f4e4 commit a4f0387

File tree

8 files changed

+359
-6
lines changed

8 files changed

+359
-6
lines changed

package-lock.json

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
},
4646
"devDependencies": {
4747
"@eslint/js": "^9.39.1",
48+
"@types/chrome": "^0.1.32",
4849
"@types/node": "^24.10.1",
4950
"@types/react": "^19.2.5",
5051
"@types/react-dom": "^19.2.3",

src/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { SidebarInset, SidebarProvider } from "./components/ui/sidebar";
66
import DigitalClock from "./components/DigitalClock";
77
import SearchBar from "./components/SearchBar";
88
import Aurora from "./components/Aurora";
9+
import ShortcutsGrid from "./components/ShortcutsGrid";
10+
import { Toaster } from "./components/Toaster";
911

1012
function App() {
1113
return (
@@ -48,8 +50,15 @@ function App() {
4850
<div className="px-6 mt-4 flex justify-center">
4951
<SearchBar />
5052
</div>
53+
54+
{/* Shortcuts Grid */}
55+
<div className="px-6 mt-10 flex justify-center">
56+
<ShortcutsGrid />
57+
</div>
58+
5159
</SidebarInset>
5260
</SidebarProvider>
61+
<Toaster position="bottom-right" />
5362
</>
5463
);
5564
}

src/components/ShortcutModal.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// src/components/ShortcutModal.tsx
2+
"use client";
3+
4+
import { useState } from "react";
5+
import { Button } from "./ui/button";
6+
import { Input } from "./ui/input";
7+
8+
type Props = {
9+
open: boolean;
10+
onClose: () => void;
11+
onAdd: (item: ShortcutItem) => void;
12+
};
13+
14+
export type ShortcutItem = {
15+
id: string;
16+
title: string;
17+
url: string;
18+
icon: string; // favicon URL
19+
};
20+
21+
22+
23+
export default function ShortcutModal({ open, onClose, onAdd }: Props) {
24+
const [title, setTitle] = useState("");
25+
const [url, setUrl] = useState("");
26+
27+
if (!open) return null;
28+
29+
// Extract favicon automatically
30+
const getFavicon = (link: string) => {
31+
try {
32+
const urlObj = new URL(link);
33+
const host = urlObj.hostname;
34+
35+
// Try Google Favicon API (works 90% cases)
36+
return `https://www.google.com/s2/favicons?sz=64&domain=${urlObj}`;
37+
} catch {
38+
return "/default-icon.png";
39+
}
40+
};
41+
42+
const normalizeUrl = (url: string) => {
43+
if (!/^https?:\/\//i.test(url)) {
44+
return `https://${url}`;
45+
}
46+
return url;
47+
};
48+
49+
const submit = () => {
50+
if (!title.trim() || !url.trim()) return;
51+
52+
const finalURL = normalizeUrl(url);
53+
const shortcut: ShortcutItem = {
54+
id: crypto.randomUUID(),
55+
title,
56+
url: finalURL,
57+
icon: getFavicon(finalURL),
58+
};
59+
60+
onAdd(shortcut);
61+
onClose();
62+
setTitle("");
63+
setUrl("");
64+
};
65+
66+
67+
return (
68+
<div
69+
className="
70+
fixed inset-0
71+
bg-black/60
72+
backdrop-blur-xl
73+
flex items-center justify-center
74+
z-50
75+
transition-all duration-200
76+
"
77+
>
78+
79+
<div
80+
className="
81+
w-full max-w-sm p-6
82+
rounded-2xl
83+
bg-white/20
84+
backdrop-blur-2xl
85+
border border-white/30
86+
shadow-[0_8px_32px_rgba(0,0,0,0.3)]
87+
text-white
88+
"
89+
>
90+
91+
<h2 className="text-xl font-semibold mb-4">Add Shortcut</h2>
92+
93+
<div className="flex flex-col gap-4">
94+
<Input
95+
className="bg-white/20 text-white placeholder:text-gray-300"
96+
placeholder="Title (e.g. YouTube)"
97+
value={title}
98+
onChange={(e) => setTitle(e.target.value)}
99+
/>
100+
101+
<Input
102+
className="bg-white/20 text-white placeholder:text-gray-300"
103+
placeholder="URL (https://...)"
104+
value={url}
105+
onChange={(e) => setUrl(e.target.value)}
106+
/>
107+
108+
<div className="flex justify-end gap-2 mt-2">
109+
<Button
110+
onClick={onClose}
111+
className="bg-white/10 hover:bg-white/20 text-white rounded-full px-4"
112+
>
113+
Cancel
114+
</Button>
115+
116+
<Button
117+
onClick={submit}
118+
className="bg-white/20 hover:bg-white/30 text-white rounded-full px-4"
119+
>
120+
Save
121+
</Button>
122+
</div>
123+
</div>
124+
</div>
125+
</div>
126+
);
127+
}

src/components/ShortcutsGrid.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import ShortcutModal from "./ShortcutModal";
5+
import type { ShortcutItem } from "./ShortcutModal"; // ✅ type-only import
6+
import { Plus } from "lucide-react";
7+
import { toast } from "sonner";
8+
9+
10+
export default function ShortcutsGrid() {
11+
const [shortcuts, setShortcuts] = useState<ShortcutItem[]>([]);
12+
const [open, setOpen] = useState(false);
13+
14+
// Load from chrome.storage.local
15+
useEffect(() => {
16+
// Handle local development where chrome API doesn't exist
17+
if (typeof chrome === "undefined" || !chrome.storage) {
18+
console.warn("chrome.storage not available");
19+
return;
20+
}
21+
22+
chrome.storage.local.get(["shortcuts"], (res) => {
23+
if (Array.isArray(res.shortcuts)) {
24+
setShortcuts(res.shortcuts);
25+
}
26+
});
27+
}, []);
28+
29+
const save = (items: ShortcutItem[]) => {
30+
setShortcuts(items);
31+
32+
if (typeof chrome !== "undefined" && chrome.storage) {
33+
chrome.storage.local.set({ shortcuts: items });
34+
}
35+
};
36+
37+
const addShortcut = (shortcut: ShortcutItem) => {
38+
if (shortcuts.length >= 14) {
39+
toast.error("Shortcut limit reached. Max 14 allowed.");
40+
return;
41+
}
42+
43+
save([...shortcuts, shortcut]);
44+
toast.success("Shortcut added!");
45+
};
46+
47+
48+
49+
return (
50+
<div className="px-6 mt-6 flex flex-col items-center w-full">
51+
52+
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-4">
53+
54+
{shortcuts.map((s) => (
55+
<div
56+
key={s.id}
57+
className="
58+
relative group
59+
flex flex-col items-center gap-2
60+
p-4 rounded-2xl
61+
bg-white/10 border border-white/20
62+
backdrop-blur-xl
63+
hover:bg-white/20 transition text-white
64+
w-24 h-24
65+
"
66+
>
67+
{/* Delete Button */}
68+
<button
69+
onClick={() => save(shortcuts.filter((x) => x.id !== s.id))}
70+
className="
71+
absolute -top-2 -right-2
72+
w-6 h-6 flex items-center justify-center
73+
rounded-full bg-red-500/80 hover:bg-red-500
74+
text-white text-xs font-bold
75+
shadow-lg opacity-0 group-hover:opacity-100
76+
transition
77+
"
78+
>
79+
×
80+
</button>
81+
82+
{/* Link wrapper */}
83+
<a
84+
href={s.url}
85+
target="_blank"
86+
rel="noopener noreferrer"
87+
className="flex flex-col items-center gap-2"
88+
>
89+
<img
90+
src={s.icon}
91+
onError={(e) => (e.currentTarget.src = "/default-icon.png")}
92+
className="w-8 h-8 rounded shadow-md bg-white/20 p-1 backdrop-blur"
93+
alt=""
94+
/>
95+
96+
<span className="text-xs truncate w-full text-center">
97+
{s.title}
98+
</span>
99+
</a>
100+
</div>
101+
102+
))}
103+
104+
{/* Add Button */}
105+
<button
106+
onClick={() => shortcuts.length < 14 && setOpen(true)}
107+
disabled={shortcuts.length >= 14}
108+
className={`
109+
flex flex-col items-center justify-center gap-2
110+
p-4 rounded-2xl
111+
bg-white/10 border border-white/20
112+
backdrop-blur-xl text-white
113+
transition w-24 h-24
114+
${shortcuts.length >= 14 ? "opacity-40 cursor-not-allowed" : "hover:bg-white/20"}
115+
`}
116+
>
117+
<Plus className="h-6 w-6" />
118+
<span className="text-xs">{shortcuts.length >= 14 ? "Limit" : "Add"}</span>
119+
</button>
120+
121+
</div>
122+
123+
{/* Modal */}
124+
<ShortcutModal
125+
open={open}
126+
onClose={() => setOpen(false)}
127+
onAdd={addShortcut}
128+
/>
129+
</div>
130+
);
131+
}

src/components/Toaster.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client";
2+
3+
import {
4+
CircleCheckIcon,
5+
InfoIcon,
6+
Loader2Icon,
7+
OctagonXIcon,
8+
TriangleAlertIcon,
9+
} from "lucide-react";
10+
11+
import { Toaster as Sonner, type ToasterProps } from "sonner";
12+
13+
const Toaster = (props: ToasterProps) => {
14+
return (
15+
<Sonner
16+
theme="light" // you can change this later
17+
className="toaster group"
18+
icons={{
19+
success: <CircleCheckIcon className="size-4" />,
20+
info: <InfoIcon className="size-4" />,
21+
warning: <TriangleAlertIcon className="size-4" />,
22+
error: <OctagonXIcon className="size-4" />,
23+
loading: <Loader2Icon className="size-4 animate-spin" />,
24+
}}
25+
style={
26+
{
27+
"--normal-bg": "rgba(255,255,255,0.2)",
28+
"--normal-text": "white",
29+
"--normal-border": "rgba(255,255,255,0.25)",
30+
"--border-radius": "1rem",
31+
backdropFilter: "blur(12px)",
32+
} as React.CSSProperties
33+
}
34+
{...props}
35+
/>
36+
);
37+
};
38+
39+
export { Toaster };

tsconfig.app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"useDefineForClassFields": true,
66
"lib": ["ES2022", "DOM", "DOM.Iterable"],
77
"module": "ESNext",
8-
"types": ["vite/client"],
8+
"types": ["vite/client", "chrome"],
99
"skipLibCheck": true,
1010

1111
/* Bundler mode */

0 commit comments

Comments
 (0)