Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script>
import { onMount } from 'svelte';
import { fade, fly } from 'svelte/transition';
import { multiChainStore } from './stores/multichain';
import { themeStore } from './stores/theme';
import { currentRoute, navigate } from './utils/router';
Expand Down Expand Up @@ -56,15 +57,19 @@
<Header on:viewChange={handleViewChange} />

<div class="container">
{#if currentView === 'explorer'}
<Explorer on:viewAddress={handleViewChange} />
{:else if currentView === 'claim'}
<AddressClaim on:viewChange={handleViewChange} />
{:else if currentView === 'address' && selectedAddress}
<AddressView address={selectedAddress} ensName={selectedENSName} on:viewChange={handleViewChange} />
{:else if currentView === 'admin'}
<AdminPanel on:viewChange={handleViewChange} />
{/if}
{#key currentView}
<div in:fly="{{ y: 20, duration: 300, delay: 150 }}" out:fade="{{ duration: 150 }}">
{#if currentView === 'explorer'}
<Explorer on:viewAddress={handleViewChange} />
{:else if currentView === 'claim'}
<AddressClaim on:viewChange={handleViewChange} />
{:else if currentView === 'address' && selectedAddress}
<AddressView address={selectedAddress} ensName={selectedENSName} on:viewChange={handleViewChange} />
{:else if currentView === 'admin'}
<AdminPanel on:viewChange={handleViewChange} />
{/if}
</div>
{/key}
</div>

<Toast />
Expand Down
162 changes: 150 additions & 12 deletions src/components/AddressClaim.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
decodeHandle,
suggestHandleIndices,
} from "../utils/wordhandles";
import EmojiPicker from './EmojiPicker.svelte';

const dispatch = createEventDispatcher();

Expand Down Expand Up @@ -409,8 +410,18 @@
return;
}

if (!formData.name) {
error = "Name is required";
if (!formData.name || formData.name.trim().length === 0) {
error = "Name is required and cannot be empty";
return;
}

if (formData.name.trim().length < 2) {
error = "Name must be at least 2 characters long";
return;
}

if (formData.website && !formData.website.match(/^https?:\/\/.+/)) {
error = "Website must be a valid URL starting with http:// or https://";
return;
Comment on lines +423 to 425
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL validation regex /^https?:\/\/.+/ is too permissive and accepts invalid URLs like 'http://' or 'https://..'. Consider using a more robust regex pattern such as /^https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-._~:/?#[\]@!$&'()*+,;=%]+$/ or the browser's built-in URL validation to properly validate URL structure.

Suggested change
if (formData.website && !formData.website.match(/^https?:\/\/.+/)) {
error = "Website must be a valid URL starting with http:// or https://";
return;
if (formData.website) {
try {
const url = new URL(formData.website);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error();
}
} catch {
error = "Website must be a valid URL starting with http:// or https://";
return;
}

Copilot uses AI. Check for mistakes.
}

Expand Down Expand Up @@ -552,17 +563,35 @@
bind:value={formData.name}
placeholder="Your Name or ENS"
required
minlength="2"
class:has-value={formData.name && formData.name.length > 0}
/>
{#if formData.name && formData.name.trim().length > 0 && formData.name.trim().length < 2}
<small class="form-hint error">Name must be at least 2 characters</small>
{:else if formData.name && formData.name.trim().length >= 2}
<small class="form-hint success">✓ Valid name</small>
{/if}
</div>

<div class="form-group">
<label for="avatar">Avatar (Emoji or URL)</label>
<input
id="avatar"
type="text"
bind:value={formData.avatar}
placeholder="😊 or https://..."
/>
<div class="avatar-input-group">
<div class="emoji-picker-container">
<EmojiPicker
selectedEmoji={formData.avatar && !formData.avatar.startsWith('http') ? formData.avatar : ''}
on:select={(e) => formData.avatar = e.detail.emoji}
/>
</div>
<div class="avatar-text-input">
<input
id="avatar"
type="text"
bind:value={formData.avatar}
placeholder="😊 or https://image-url.com"
/>
</div>
</div>
<small class="form-hint">Choose an emoji or paste an image URL</small>
</div>

<div class="form-group">
Expand All @@ -582,7 +611,13 @@
type="url"
bind:value={formData.website}
placeholder="https://yourwebsite.com"
class:has-value={formData.website && formData.website.length > 0}
/>
{#if formData.website && formData.website.length > 0 && !formData.website.match(/^https?:\/\/.+/)}
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL validation regex /^https?:\/\/.+/ is too permissive and accepts invalid URLs like 'http://' or 'https://..'. Use the same robust regex pattern as in the validation function (line 423) for consistency.

Copilot uses AI. Check for mistakes.
<small class="form-hint error">URL must start with http:// or https://</small>
{:else if formData.website && formData.website.match(/^https?:\/\/.+/)}
<small class="form-hint success">✓ Valid URL</small>
{/if}
</div>

<div class="form-row">
Expand Down Expand Up @@ -999,6 +1034,25 @@
gap: 1rem;
}

.avatar-input-group {
display: grid;
grid-template-columns: 120px 1fr;
gap: 1rem;
align-items: start;
}

.emoji-picker-container {
width: 100%;
}

.avatar-text-input {
flex: 1;
}

.avatar-text-input input {
width: 100%;
}

label {
display: block;
margin-bottom: 0.5rem;
Expand Down Expand Up @@ -1052,12 +1106,41 @@
margin-top: 0.25rem;
font-size: 0.875rem;
color: #64748b;
transition: all 0.2s ease;
}

.claim-container.dark .form-hint {
color: #94a3b8;
}

.form-hint.error {
color: #dc2626;
font-weight: 500;
}

.claim-container.dark .form-hint.error {
color: #fca5a5;
}

.form-hint.success {
color: #059669;
font-weight: 500;
}

.claim-container.dark .form-hint.success {
color: #6ee7b7;
}

input.has-value,
textarea.has-value {
border-color: #94a3b8;
}

.claim-container.dark input.has-value,
.claim-container.dark textarea.has-value {
border-color: #64748b;
}

.checkbox-group label {
display: flex;
align-items: center;
Expand Down Expand Up @@ -1118,8 +1201,16 @@
}

.btn-claim:disabled {
opacity: 0.6;
opacity: 0.5;
cursor: not-allowed;
background: #94a3b8;
transform: none;
box-shadow: none;
}

.claim-container.dark .btn-claim:disabled {
background: #475569;
color: #94a3b8;
}

.handle-section {
Expand Down Expand Up @@ -1295,8 +1386,16 @@
}

.btn-secondary:disabled {
opacity: 0.6;
opacity: 0.5;
cursor: not-allowed;
background: #f1f5f9;
color: #94a3b8;
transform: none;
}

.claim-container.dark .btn-secondary:disabled {
background: #1e293b;
color: #64748b;
}

.btn-handle-claim {
Expand All @@ -1318,8 +1417,16 @@
}

.btn-handle-claim:disabled {
opacity: 0.6;
opacity: 0.5;
cursor: not-allowed;
background: #94a3b8;
transform: none;
box-shadow: none;
}

.claim-container.dark .btn-handle-claim:disabled {
background: #475569;
color: #94a3b8;
}

.btn-release {
Expand All @@ -1341,8 +1448,17 @@
}

.btn-release:disabled {
opacity: 0.6;
opacity: 0.5;
cursor: not-allowed;
background: #f1f5f9;
color: #94a3b8;
border-color: #e2e8f0;
}

.claim-container.dark .btn-release:disabled {
background: #1e293b;
color: #64748b;
border-color: #334155;
}

.info-box {
Expand Down Expand Up @@ -1407,5 +1523,27 @@
.claim-form {
padding: 1.5rem;
}

.avatar-input-group {
grid-template-columns: 1fr;
}

.handle-header {
flex-direction: column;
align-items: flex-start;
}

.handle-caption {
justify-content: flex-start;
}

.handle-actions {
flex-direction: column;
}

.btn-secondary,
.btn-handle-claim {
width: 100%;
}
}
</style>
Loading
Loading