-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Add 10 new educational tools using Svelte 5 runes and shadcn components #546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Deploying edutools-testing with
|
| Latest commit: |
48a061a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://8945d216.edutools-testing.pages.dev |
| Branch Preview URL: | https://copilot-fix-f1acd065-f61d-45.edutools-testing.pages.dev |
…mponents Co-authored-by: Inglan <96573515+Inglan@users.noreply.github.com>
WalkthroughAdds multiple new tool pages under /tools (ASCII Art, Base64 Converter, Color Picker, Hash Generator, JSON Formatter, Lorem Generator, Markdown Preview, QR Code Generator, URL Tools, Regex Tester). Updates navigation and the home tools list to include these entries. No public API changes; all updates are UI/routes and local logic. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant P as URL Tools Page
participant A as TinyURL API
rect rgb(245,248,255)
note over U,P: Shorten flow
U->>P: Enter URL + click "Shorten"
P->>P: Validate URL
alt valid
P->>A: POST/GET shorten request
A-->>P: Short URL or error
alt success
P-->>U: Display shortened URL (copy option)
else failure
P-->>U: Show network/API error
end
else invalid
P-->>U: Show validation error
end
end
rect rgb(245,255,247)
note over U,P: Analyze flow
U->>P: Enter URL (or edit)
P->>P: Parse via URL(...)
P-->>U: Show components (protocol, host, path, etc.)
end
sequenceDiagram
autonumber
actor U as User
participant B as Base64 Converter Page
U->>B: Type/paste text
alt Encode tab
U->>B: Click "Encode"
B->>B: btoa / UTF-8 handling
B-->>U: Show Base64 or error
else Decode tab
U->>B: Click "Decode"
B->>B: atob / UTF-8 handling
B-->>U: Show decoded text or error
end
U->>B: Click "Copy Result"
B-->>U: Clipboard updated
sequenceDiagram
autonumber
actor U as User
participant H as Hash Generator Page
participant C as SubtleCrypto
U->>H: Type input text
H->>H: Compute MD5 (lib) and CRC32 (inline)
H->>C: digest(SHA-1)
C-->>H: SHA-1 hex
H->>C: digest(SHA-256)
C-->>H: SHA-256 hex
H-->>U: Display all hashes + copy buttons
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@copilot a few comments:
|
…atures, fix layout issues Co-authored-by: Inglan <96573515+Inglan@users.noreply.github.com>
All feedback addressed! ✅ Changes made in commit
All tools now have cleaner UIs and the problematic features have been removed. The copy buttons show proper toast confirmations and the layouts are much more contained and user-friendly. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
src/lib/navigation.ts(1 hunks)src/routes/+page.svelte(1 hunks)src/routes/tools/ascii-art/+page.svelte(1 hunks)src/routes/tools/base64-converter/+page.svelte(1 hunks)src/routes/tools/color-picker/+page.svelte(1 hunks)src/routes/tools/hash-generator/+page.svelte(1 hunks)src/routes/tools/json-formatter/+page.svelte(1 hunks)src/routes/tools/lorem-generator/+page.svelte(1 hunks)src/routes/tools/markdown-preview/+page.svelte(1 hunks)src/routes/tools/qr-code-generator/+page.svelte(1 hunks)src/routes/tools/regex-tester/+page.svelte(1 hunks)src/routes/tools/url-tools/+page.svelte(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.*
⚙️ CodeRabbit configuration file
**/*.*: Do not correct spelling errors or grammar mistakes.
Files:
src/routes/+page.sveltesrc/routes/tools/url-tools/+page.sveltesrc/routes/tools/hash-generator/+page.sveltesrc/lib/navigation.tssrc/routes/tools/ascii-art/+page.sveltesrc/routes/tools/regex-tester/+page.sveltesrc/routes/tools/markdown-preview/+page.sveltesrc/routes/tools/base64-converter/+page.sveltesrc/routes/tools/lorem-generator/+page.sveltesrc/routes/tools/qr-code-generator/+page.sveltesrc/routes/tools/json-formatter/+page.sveltesrc/routes/tools/color-picker/+page.svelte
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: EducationalTools/src#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-23T23:52:45.385Z
Learning: Applies to src/routes/+page.svelte : Update the tools list in src/routes/+page.svelte when adding a new tool
📚 Learning: 2025-09-23T23:52:45.385Z
Learnt from: CR
PR: EducationalTools/src#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-23T23:52:45.385Z
Learning: Applies to src/routes/+page.svelte : Update the tools list in src/routes/+page.svelte when adding a new tool
Applied to files:
src/routes/+page.sveltesrc/routes/tools/url-tools/+page.sveltesrc/routes/tools/hash-generator/+page.sveltesrc/routes/tools/markdown-preview/+page.sveltesrc/routes/tools/lorem-generator/+page.sveltesrc/routes/tools/qr-code-generator/+page.sveltesrc/routes/tools/json-formatter/+page.sveltesrc/routes/tools/color-picker/+page.svelte
📚 Learning: 2025-09-23T23:52:45.385Z
Learnt from: CR
PR: EducationalTools/src#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-23T23:52:45.385Z
Learning: Applies to src/routes/tools/*/+page.svelte : Add new educational tools under src/routes/tools/[tool-name]/ with a +page.svelte implementation
Applied to files:
src/routes/+page.sveltesrc/routes/tools/url-tools/+page.sveltesrc/routes/tools/hash-generator/+page.sveltesrc/routes/tools/ascii-art/+page.sveltesrc/routes/tools/regex-tester/+page.sveltesrc/routes/tools/markdown-preview/+page.sveltesrc/routes/tools/base64-converter/+page.sveltesrc/routes/tools/lorem-generator/+page.sveltesrc/routes/tools/qr-code-generator/+page.sveltesrc/routes/tools/json-formatter/+page.sveltesrc/routes/tools/color-picker/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
src/routes/tools/ascii-art/+page.svelte (1)
68-100: Solid ASCII-art rendering loopUppercasing input, falling back to the space glyph, and spacing characters per line keeps the output tidy and deterministic. Nice polish.
| <Tabs value="encode" class="w-full"> | ||
| <TabsList class="grid w-full grid-cols-2"> | ||
| <TabsTrigger value="encode">Encode</TabsTrigger> | ||
| <TabsTrigger value="decode">Decode</TabsTrigger> | ||
| </TabsList> | ||
|
|
||
| <TabsContent value="encode" class="space-y-4"> | ||
| <div> | ||
| <Textarea | ||
| bind:value={inputText} | ||
| placeholder="Enter text to encode to Base64..." | ||
| class="min-h-[150px]" | ||
| /> | ||
| </div> | ||
| <Button onclick={encode} disabled={!inputText.trim()}>Encode to Base64</Button> | ||
| </TabsContent> | ||
|
|
||
| <TabsContent value="decode" class="space-y-4"> | ||
| <div> | ||
| <Textarea | ||
| bind:value={inputText} | ||
| placeholder="Enter Base64 text to decode..." | ||
| class="min-h-[150px]" | ||
| /> | ||
| </div> | ||
| <Button onclick={decode} disabled={!inputText.trim()}>Decode from Base64</Button> | ||
| </TabsContent> | ||
| </Tabs> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow the tabs to switch so the Decode tool is usable.
<Tabs value="encode"> locks the component to the Encode tab; clicking “Decode” never changes the active tab, so the decoding workflow can’t be reached. Use defaultValue="encode" (or bind value to state) so the tab set can update on user interaction.
- <Tabs value="encode" class="w-full">
+ <Tabs defaultValue="encode" class="w-full">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Tabs value="encode" class="w-full"> | |
| <TabsList class="grid w-full grid-cols-2"> | |
| <TabsTrigger value="encode">Encode</TabsTrigger> | |
| <TabsTrigger value="decode">Decode</TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="encode" class="space-y-4"> | |
| <div> | |
| <Textarea | |
| bind:value={inputText} | |
| placeholder="Enter text to encode to Base64..." | |
| class="min-h-[150px]" | |
| /> | |
| </div> | |
| <Button onclick={encode} disabled={!inputText.trim()}>Encode to Base64</Button> | |
| </TabsContent> | |
| <TabsContent value="decode" class="space-y-4"> | |
| <div> | |
| <Textarea | |
| bind:value={inputText} | |
| placeholder="Enter Base64 text to decode..." | |
| class="min-h-[150px]" | |
| /> | |
| </div> | |
| <Button onclick={decode} disabled={!inputText.trim()}>Decode from Base64</Button> | |
| </TabsContent> | |
| </Tabs> | |
| <Tabs defaultValue="encode" class="w-full"> | |
| <TabsList class="grid w-full grid-cols-2"> | |
| <TabsTrigger value="encode">Encode</TabsTrigger> | |
| <TabsTrigger value="decode">Decode</TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="encode" class="space-y-4"> | |
| <div> | |
| <Textarea | |
| bind:value={inputText} | |
| placeholder="Enter text to encode to Base64..." | |
| class="min-h-[150px]" | |
| /> | |
| </div> | |
| <Button onclick={encode} disabled={!inputText.trim()}>Encode to Base64</Button> | |
| </TabsContent> | |
| <TabsContent value="decode" class="space-y-4"> | |
| <div> | |
| <Textarea | |
| bind:value={inputText} | |
| placeholder="Enter Base64 text to decode..." | |
| class="min-h-[150px]" | |
| /> | |
| </div> | |
| <Button onclick={decode} disabled={!inputText.trim()}>Decode from Base64</Button> | |
| </TabsContent> | |
| </Tabs> |
🤖 Prompt for AI Agents
In src/routes/tools/base64-converter/+page.svelte around lines 57 to 84, the
Tabs component is hard-locked to the Encode tab because it uses a static
value="encode", preventing users from switching to Decode; change it to use
defaultValue="encode" or bind the Tabs value to component state (e.g.,
bind:value={activeTab}) so the tab selection can update on user interaction, and
ensure any existing encode/decode handlers use the same bound state if needed.
| async function generateAllHashes() { | ||
| if (!inputText.trim()) { | ||
| hashes = {}; | ||
| return; | ||
| } | ||
|
|
||
| const newHashes: Record<string, string> = {}; | ||
|
|
||
| // Generate all hashes | ||
| newHashes.MD5 = generateMD5(inputText); | ||
| newHashes.SHA1 = await generateSHA1(inputText); | ||
| newHashes.SHA256 = await generateSHA256(inputText); | ||
| newHashes.CRC32 = generateCRC32(inputText); | ||
|
|
||
| hashes = newHashes; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent stale hashes when multiple async digests overlap
generateAllHashes is async and runs on every keystroke via $effect. If a slower digest from an earlier input resolves after a newer one, it overwrites hashes with stale results. Please capture the current input and bail out if it has changed before committing the hash map.
-async function generateAllHashes() {
- if (!inputText.trim()) {
- hashes = {};
- return;
- }
-
- const newHashes: Record<string, string> = {};
-
- // Generate all hashes
- newHashes.MD5 = generateMD5(inputText);
- newHashes.SHA1 = await generateSHA1(inputText);
- newHashes.SHA256 = await generateSHA256(inputText);
- newHashes.CRC32 = generateCRC32(inputText);
-
- hashes = newHashes;
-}
+async function generateAllHashes() {
+ const currentText = inputText;
+ if (!currentText.trim()) {
+ hashes = {};
+ return;
+ }
+
+ const newHashes: Record<string, string> = {};
+ newHashes.MD5 = generateMD5(currentText);
+ newHashes.SHA1 = await generateSHA1(currentText);
+ newHashes.SHA256 = await generateSHA256(currentText);
+ newHashes.CRC32 = generateCRC32(currentText);
+
+ if (currentText !== inputText) {
+ return;
+ }
+
+ hashes = newHashes;
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function generateAllHashes() { | |
| if (!inputText.trim()) { | |
| hashes = {}; | |
| return; | |
| } | |
| const newHashes: Record<string, string> = {}; | |
| // Generate all hashes | |
| newHashes.MD5 = generateMD5(inputText); | |
| newHashes.SHA1 = await generateSHA1(inputText); | |
| newHashes.SHA256 = await generateSHA256(inputText); | |
| newHashes.CRC32 = generateCRC32(inputText); | |
| hashes = newHashes; | |
| } | |
| async function generateAllHashes() { | |
| const currentText = inputText; | |
| if (!currentText.trim()) { | |
| hashes = {}; | |
| return; | |
| } | |
| const newHashes: Record<string, string> = {}; | |
| newHashes.MD5 = generateMD5(currentText); | |
| newHashes.SHA1 = await generateSHA1(currentText); | |
| newHashes.SHA256 = await generateSHA256(currentText); | |
| newHashes.CRC32 = generateCRC32(currentText); | |
| // Bail out if inputText has changed since we started | |
| if (currentText !== inputText) { | |
| return; | |
| } | |
| hashes = newHashes; | |
| } |
🤖 Prompt for AI Agents
In src/routes/tools/hash-generator/+page.svelte around lines 56 to 71,
generateAllHashes can commit stale results if earlier async digests resolve
after newer inputs; capture the current input at the start (e.g., const snapshot
= inputText) and use that snapshot for all digest generation; run async digests
as promises (or start them immediately), await them, then before setting hashes
check if inputText === snapshot (or if snapshot.trim() is still equal) and bail
out if changed so stale results are not assigned to hashes.
| outputJson = JSON.stringify(parsed, null, indentSize); | ||
| isValid = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep the indent size numeric when formatting JSON.
Line 22 currently passes indentSize straight into JSON.stringify. After the user touches the dropdown (Line 137), indentSize becomes a string ('2', '4', …), so the output starts indenting with the digit character instead of spaces. Convert the value back to a number before calling JSON.stringify (and likewise wherever it’s reused) so formatting remains correct.
function formatJson() {
try {
- const parsed = JSON.parse(inputJson);
- outputJson = JSON.stringify(parsed, null, indentSize);
+ const parsed = JSON.parse(inputJson);
+ const indent = Number(indentSize) || 0;
+ outputJson = JSON.stringify(parsed, null, indent);
isValid = true;
error = '';
} catch (e) {…and ensure the same numeric coercion is used wherever the indent value is consumed.
Also applies to: 137-141
🤖 Prompt for AI Agents
In src/routes/tools/json-formatter/+page.svelte around lines 22-23 and 137-141,
the indentSize is being passed as a string after the dropdown change which
causes JSON.stringify to use the character instead of spaces; convert/coerce
indentSize to a number (for example with Number(indentSize) or
parseInt(indentSize, 10)) before calling JSON.stringify and wherever indentSize
is reused, validate/fallback to a numeric default if coercion yields NaN so
formatting remains correct.
| // Bold and Italic | ||
| html = html.replace(/\*\*\*(.*)\*\*\*/gim, '<strong><em>$1</em></strong>'); | ||
| html = html.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>'); | ||
| html = html.replace(/\*(.*)\*/gim, '<em>$1</em>'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make the emphasis patterns non-greedy
**one** and **two** currently becomes a single <strong> block because the (.*) matches across the whole span. The same happens for italics and triple emphasis. Use non-greedy quantifiers so each pair of markers scopes correctly.
- html = html.replace(/\*\*\*(.*)\*\*\*/gim, '<strong><em>$1</em></strong>');
- html = html.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>');
- html = html.replace(/\*(.*)\*/gim, '<em>$1</em>');
+ html = html.replace(/\*\*\*(.*?)\*\*\*/gim, '<strong><em>$1</em></strong>');
+ html = html.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>');
+ html = html.replace(/\*(.*?)\*/gim, '<em>$1</em>');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Bold and Italic | |
| html = html.replace(/\*\*\*(.*)\*\*\*/gim, '<strong><em>$1</em></strong>'); | |
| html = html.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>'); | |
| html = html.replace(/\*(.*)\*/gim, '<em>$1</em>'); | |
| // Bold and Italic | |
| html = html.replace(/\*\*\*(.*?)\*\*\*/gim, '<strong><em>$1</em></strong>'); | |
| html = html.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>'); | |
| html = html.replace(/\*(.*?)\*/gim, '<em>$1</em>'); |
| html = html.replace(/^\* (.+$)/gim, '<li>$1</li>'); | ||
| html = html.replace(/^\d+\. (.+$)/gim, '<li>$1</li>'); | ||
|
|
||
| // Wrap consecutive <li> items in <ul> or <ol> | ||
| html = html.replace(/(<li>.*<\/li>)\n(?=<li>)/gim, '$1'); | ||
| html = html.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>'); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix unordered list rendering
The converter only recognises * bullets, so the default - Item examples render as plain text. The follow-up wrapper also nests every <li> in its own <ul>, breaking list semantics. Broaden the matcher to cover -/*/+ and wrap contiguous list items together.
- // Lists
- html = html.replace(/^\* (.+$)/gim, '<li>$1</li>');
- html = html.replace(/^\d+\. (.+$)/gim, '<li>$1</li>');
-
- // Wrap consecutive <li> items in <ul> or <ol>
- html = html.replace(/(<li>.*<\/li>)\n(?=<li>)/gim, '$1');
- html = html.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>');
+ // Lists
+ html = html.replace(/^\s*[-*+] (.+)$/gim, '<li>$1</li>');
+ html = html.replace(/^\s*\d+\.\s+(.+)$/gim, '<li>$1</li>');
+
+ // Wrap consecutive <li> items in one list block
+ html = html.replace(/(?:<li>.*<\/li>\s*)+/gim, (list) => `<ul>${list.replace(/\s+/g, ' ').trim()}</ul>`);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| html = html.replace(/^\* (.+$)/gim, '<li>$1</li>'); | |
| html = html.replace(/^\d+\. (.+$)/gim, '<li>$1</li>'); | |
| // Wrap consecutive <li> items in <ul> or <ol> | |
| html = html.replace(/(<li>.*<\/li>)\n(?=<li>)/gim, '$1'); | |
| html = html.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>'); | |
| // Lists | |
| html = html.replace(/^\s*[-*+] (.+)$/gim, '<li>$1</li>'); | |
| html = html.replace(/^\s*\d+\.\s+(.+)$/gim, '<li>$1</li>'); | |
| // Wrap consecutive <li> items in one list block | |
| html = html.replace( | |
| /(?:<li>.*<\/li>\s*)+/gim, | |
| (list) => `<ul>${list.replace(/\s+/g, ' ').trim()}</ul>` | |
| ); |
🤖 Prompt for AI Agents
In src/routes/tools/markdown-preview/+page.svelte around lines 128 to 134, the
list handling only matches '*' bullets and wraps each <li> in its own <ul>,
breaking semantics; change the bullet matcher to accept -, *, + for unordered
and keep the numeric matcher for ordered, produce <li> elements without
surrounding lists, then replace contiguous runs of <li> nodes with a single
wrapper by detecting sequences that originate from unordered markers and wrap
those sequences in one <ul> and sequences from ordered markers in one <ol> (use
regex with lookahead/anchors or a replace callback to group adjacent <li>
occurrences and wrap them together rather than wrapping each <li> individually).
| function downloadQR() { | ||
| if (qrCodeUrl) { | ||
| const link = document.createElement('a'); | ||
| link.href = qrCodeUrl; | ||
| link.download = 'qrcode.png'; | ||
| link.click(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure “Download QR Code” works cross-origin
The current downloadQR() relies on <a download> pointing at api.qrserver.com, but browsers ignore download for cross-origin URLs. The button just opens the PNG in a new tab, so users never receive a file download. Please fetch the image as a blob (or proxy it through our backend) and create an object URL before triggering the download to guarantee the promised behavior.
-function downloadQR() {
- if (qrCodeUrl) {
- const link = document.createElement('a');
- link.href = qrCodeUrl;
- link.download = 'qrcode.png';
- link.click();
- }
-}
+async function downloadQR() {
+ if (!qrCodeUrl) return;
+
+ try {
+ const response = await fetch(qrCodeUrl);
+ if (!response.ok) throw new Error(`Failed to fetch QR code: ${response.status}`);
+
+ const blob = await response.blob();
+ const objectUrl = URL.createObjectURL(blob);
+
+ const link = document.createElement('a');
+ link.href = objectUrl;
+ link.download = 'qrcode.png';
+ link.click();
+
+ URL.revokeObjectURL(objectUrl);
+ } catch (error) {
+ console.error('Unable to download QR code', error);
+ }
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function downloadQR() { | |
| if (qrCodeUrl) { | |
| const link = document.createElement('a'); | |
| link.href = qrCodeUrl; | |
| link.download = 'qrcode.png'; | |
| link.click(); | |
| } | |
| async function downloadQR() { | |
| if (!qrCodeUrl) return; | |
| try { | |
| const response = await fetch(qrCodeUrl); | |
| if (!response.ok) throw new Error(`Failed to fetch QR code: ${response.status}`); | |
| const blob = await response.blob(); | |
| const objectUrl = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = objectUrl; | |
| link.download = 'qrcode.png'; | |
| link.click(); | |
| URL.revokeObjectURL(objectUrl); | |
| } catch (error) { | |
| console.error('Unable to download QR code', error); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/routes/tools/qr-code-generator/+page.svelte around lines 29–35, the
downloadQR function relies on a cross-origin href which prevents the download
attribute from working; replace the direct link click with a fetch of qrCodeUrl,
await response.blob(), create an object URL from the blob, set link.href to that
object URL and link.download to 'qrcode.png', trigger link.click(), then revoke
the object URL; if fetch fails due to CORS, fallback to proxying the request
through our backend endpoint and perform the same blob -> object URL flow.
| const regex = new RegExp(pattern, flags); | ||
| const allMatches = Array.from(testString.matchAll(regex)); | ||
| matches = allMatches; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't mark valid non‑global regexes as invalid
When the user removes the g flag, String.prototype.matchAll throws and we surface that as an “invalid regex”, so perfectly valid patterns (e.g. lookaheads, anchors) can’t be tested. Clone the pattern with a global flag just for matching so we keep the original flags for display while still iterating safely.
- const regex = new RegExp(pattern, flags);
- const allMatches = Array.from(testString.matchAll(regex));
+ const regex = new RegExp(pattern, flags);
+ const normalizedFlags = flags.includes('g') ? flags : `${flags}g`;
+ const workingRegex = normalizedFlags === flags ? regex : new RegExp(pattern, normalizedFlags);
+ const allMatches = Array.from(testString.matchAll(workingRegex));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const regex = new RegExp(pattern, flags); | |
| const allMatches = Array.from(testString.matchAll(regex)); | |
| matches = allMatches; | |
| const regex = new RegExp(pattern, flags); | |
| const normalizedFlags = flags.includes('g') ? flags : `${flags}g`; | |
| const workingRegex = normalizedFlags === flags ? regex : new RegExp(pattern, normalizedFlags); | |
| const allMatches = Array.from(testString.matchAll(workingRegex)); | |
| matches = allMatches; |
🤖 Prompt for AI Agents
In src/routes/tools/regex-tester/+page.svelte around lines 78 to 80, the code
uses matchAll on the user-created RegExp which throws when the user omitted the
global flag; create a separate RegExp instance that guarantees the 'g' flag for
matching (e.g. if flags already include 'g' use them, otherwise append 'g') and
call matchAll on that instance while keeping the original RegExp (and its flags)
unchanged for display; assign matches from Array.from(matchAll) on the cloned
global regex and leave the original regex object intact for UI/printing.
| const response = await fetch( | ||
| `https://tinyurl.com/api-create.php?url=${encodeURIComponent(inputUrl)}` | ||
| ); | ||
| const shortUrl = await response.text(); | ||
|
|
||
| if (shortUrl.startsWith('https://tinyurl.com/')) { | ||
| outputUrl = shortUrl; | ||
| } else { | ||
| error = 'Failed to shorten URL. Please try again.'; | ||
| } | ||
| } catch (e) { | ||
| error = 'Network error. Please check your connection and try again.'; | ||
| } finally { | ||
| loading = false; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the TinyURL call — browser requests are blocked.
In browsers this fetch never succeeds: tinyurl.com's API does not send CORS headers, so the request is rejected before we can read the response, leaving the shortening tab stuck on “Network error”. (freeapis.io)
Please proxy the call through our backend or switch to a shortening API that explicitly allows cross-origin access from the client.

This PR significantly expands the EduTools platform by adding 10 new educational tools, bringing the total from 7 to 17 tools. All new tools are built using Svelte 5 runes (
$state,$derived,$effect) and leverage the existing shadcn component library for consistent UI/UX.🆕 New Tools Added
QR Code Generator (
/tools/qr-code-generator)Base64 Encoder/Decoder (
/tools/base64-converter)JSON Formatter/Validator (
/tools/json-formatter)Color Picker & Palette (
/tools/color-picker)URL Tools (
/tools/url-tools)Hash Generator (
/tools/hash-generator)Lorem Ipsum Generator (
/tools/lorem-generator)ASCII Art Generator (
/tools/ascii-art)Markdown Preview (
/tools/markdown-preview)Regex Tester (
/tools/regex-tester)🏗️ Technical Implementation
$statefor reactive variables,$derivedfor computed values, and$effectfor side effects/tools/[tool-name]/+page.sveltepattern🔧 UI/UX Improvements Made
Based on feedback, several tools were refined for better user experience:
🔍 Quality Assurance
pnpm run formatandpnpm run lintpnpm run build(32-second build time)📸 Screenshots
Color Picker with enhanced copy buttons and toast notifications:

URL Tools with streamlined interface:

Lorem Generator simplified to paragraphs only:

ASCII Art with clean, focused interface:

Markdown Preview with fixed container layout:

📁 Files Changed
src/routes/+page.svelteto include all new toolssrc/lib/navigation.tsto add new tools to navigationsrc/routes/tools/This expansion significantly enhances the educational value of the platform while maintaining code quality and consistency with existing patterns. The tools have been refined based on user feedback to provide cleaner, more focused experiences.
Original prompt
💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.
Summary by CodeRabbit
New Features
UI