diff --git a/.gitignore b/.gitignore index a276f12..416620f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ node_modules/ .pnp .pnp.js +package-lock.json +yarn.lock # Build outputs dist/ @@ -14,13 +16,18 @@ build/ .env .env.local .env.*.local +.env.development +.env.production +.env.staging -# IDE +# IDE & Cache .vscode/ .idea/ *.swp *.swo *~ +.eslintcache +*.tsbuildinfo # OS .DS_Store @@ -48,3 +55,7 @@ yarn-debug.log* pnpm-debug.log* .cache/ tmp/ +.vercel/ +.netlify/ +.wrangler/ +vite.config.*.timestamp-* diff --git a/README.md b/README.md index 136600f..08c540d 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,19 @@ devcard/ | Web Backup | SvelteKit | | Auth | OAuth 2.0 (GitHub, Google) | +### Web Application (`apps/web`) + +The web app is a client-side SvelteKit application built with Svelte 5 and Vite. It runs independently of any backend or database using `localStorage` for all persistent storage. + +- **Developer Card Form**: Create or update your profile card containing fields for Username, Full Name, Bio, and multiple platform links (GitHub, LinkedIn, Twitter/X, Instagram, YouTube, Dev.to, LeetCode, Portfolio). +- **Interactive Avatar Studio**: + - **Dynamic Initials SVG**: Instantly generates a colorful gradient SVG avatar using the initials from the entered Full Name. + - **DiceBear Suggestions**: Real-time suggested avatars generated using DiceBear APIs (*Avataaars*, *Robots*, *Pixel Art*, *Chibi*, *Identicon*) dynamically seeded by the username and display name. + - **GitHub Sync**: One-click sync to fetch the user's GitHub avatar once they add their GitHub link. + - **Presets**: High-quality developer photography presets from Unsplash. +- **Card Sharing & QR Codes**: Generates a shareable card at `/u/[username]`. The page renders the profile details, connection tiles, and a QR code of its own URL. Includes options to download the QR code as a PNG, copy the profile link, and edit the card. +- **Preloaded Demo**: Automatically pre-seeds a demo developer profile at `/u/devcard-demo` for instant testing. + ### Hybrid Follow Engine DevCard uses a three-layer follow engine: diff --git a/apps/web/package.json b/apps/web/package.json index 3601215..13229d6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,12 +12,14 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" }, "dependencies": { - "@devcard/shared": "workspace:*" + "@devcard/shared": "workspace:*", + "qrcode": "^1.5.4" }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/qrcode": "^1.5.6", "svelte": "^5.51.0", "svelte-check": "^4.4.2", "typescript": "^5.9.3", diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte index 11f8085..6a0c911 100644 --- a/apps/web/src/routes/+page.svelte +++ b/apps/web/src/routes/+page.svelte @@ -1,9 +1,118 @@ - @@ -37,7 +341,7 @@ ⭐ Star on GitHub - View Demo Profile β†’ + +
+
+
⚑
+

{isEditing ? 'Update Your DevCard' : 'Create Your DevCard'}

+

+ Enter your details to generate a stunning, shareable developer profile card. +

+
+ + {#if formError} + + {/if} + + {#if formSuccess} + + {/if} + +
{ e.preventDefault(); generateCard(); }} class="card-form"> +
+ +
+ +
+ /u/ + +
+ {#if !isEditing} + Your card will be available at /u/your-username + {:else} + Username cannot be changed when editing + {/if} +
+ + +
+ + +
+ + +
+ Avatar Studio (Pick an avatar suggestion below) + +
+ +
+
+ {#if avatarUrl && (avatarUrl.startsWith('data:') || /^https?:\/\//i.test(avatarUrl))} + Avatar Preview { avatarUrl = ''; }} /> + {:else} +
+ {displayName ? displayName.charAt(0).toUpperCase() : '⚑'} +
+ {/if} +
+ Live Preview +
+ + +
+ +
+ ✨ Dynamic Suggestions (Real-time) +
+ + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+ + +
+ + + +
+
+ + + + + +
+ + {#if isEditing} + + {/if} +
+
+
+
πŸ”—
@@ -196,9 +799,28 @@ display: flex; gap: 1rem; justify-content: center; + align-items: center; flex-wrap: wrap; } + .btn-outline { + padding: 0.92rem 1.75rem; + border-radius: calc(var(--radius) * 1.15); + font-weight: 700; + border: 1px solid rgba(255, 255, 255, 0.18); + background: transparent; + color: var(--text-primary); + transition: all 0.2s ease; + text-decoration: none; + display: inline-flex; + align-items: center; + } + + .btn-outline:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(99, 102, 241, 0.35); + } + .btn-secondary { padding: 0.92rem 1.75rem; border-radius: calc(var(--radius) * 1.15); @@ -206,6 +828,11 @@ border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.08); color: var(--text-primary); + cursor: pointer; + font: inherit; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; } .btn-secondary:hover { @@ -213,6 +840,663 @@ border-color: rgba(99, 102, 241, 0.45); } + /* --- Create Card Form Styles --- */ + .create-card-section { + padding: clamp(2rem, 5vw, 3.5rem) clamp(1.5rem, 4vw, 3rem); + border-radius: var(--radius-xl, 24px); + margin: 3rem auto 5rem; + background: rgba(15, 23, 42, 0.7); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: var(--shadow-xl); + max-width: 800px; + position: relative; + overflow: hidden; + } + + .section-header { + text-align: center; + margin-bottom: 2.5rem; + } + + .logo-accent { + font-size: 2.5rem; + margin-bottom: 0.5rem; + display: inline-block; + animation: pulse 2s infinite; + } + + .create-card-section h2 { + font-size: clamp(1.8rem, 3.2vw, 2.5rem); + font-weight: 800; + margin-bottom: 0.75rem; + background: linear-gradient(135deg, #fff 0%, var(--text-secondary) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + + .section-subtitle { + color: var(--text-secondary); + font-size: 1.05rem; + max-width: 550px; + margin: 0 auto; + line-height: 1.5; + } + + .section-subtitle-sm { + color: var(--text-muted); + font-size: 0.9rem; + margin-bottom: 1.25rem; + line-height: 1.4; + } + + .card-form { + display: flex; + flex-direction: column; + gap: 2rem; + } + + .form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; + } + + @media (max-width: 640px) { + .form-grid { + grid-template-columns: 1fr; + } + } + + .form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .form-group.full-width { + grid-column: span 2; + } + + @media (max-width: 640px) { + .form-group.full-width { + grid-column: span 1; + } + } + + label, .studio-title-label { + font-size: 0.9rem; + font-weight: 600; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 0.25rem; + } + + .required { + color: #ef4444; + } + + .optional { + color: var(--text-muted); + font-size: 0.8rem; + font-weight: normal; + } + + input, select, textarea { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--radius); + padding: 0.75rem 1rem; + color: var(--text-primary); + font-size: 1rem; + font-family: inherit; + transition: all 0.2s ease; + } + + input:focus, select:focus, textarea:focus { + outline: none; + border-color: rgba(99, 102, 241, 0.5); + background: rgba(255, 255, 255, 0.08); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); + } + + .input-with-prefix { + display: flex; + align-items: stretch; + border-radius: var(--radius); + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + overflow: hidden; + transition: all 0.2s ease; + } + + .input-with-prefix:focus-within { + border-color: rgba(99, 102, 241, 0.5); + background: rgba(255, 255, 255, 0.08); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); + } + + .input-with-prefix input { + border: none; + background: transparent; + padding-left: 0.35rem; + flex: 1; + box-shadow: none; + } + + .input-with-prefix input:focus { + border: none; + box-shadow: none; + background: transparent; + } + + .url-prefix { + display: flex; + align-items: center; + padding: 0 0.75rem; + background: rgba(255, 255, 255, 0.04); + color: var(--text-muted); + font-size: 0.95rem; + border-right: 1px solid rgba(255, 255, 255, 0.08); + font-weight: 500; + } + + .input-helper { + font-size: 0.8rem; + color: var(--text-muted); + } + + /* --- Avatar Studio Styles --- */ + .avatar-studio-group { + margin-bottom: 0.5rem; + } + + .avatar-studio-container { + display: flex; + gap: 2rem; + padding: 1.5rem; + border-radius: var(--radius-xl); + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.06); + align-items: center; + } + + @media (max-width: 640px) { + .avatar-studio-container { + flex-direction: column; + align-items: center; + gap: 1.5rem; + } + } + + .avatar-preview-box { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + min-width: 120px; + } + + .preview-avatar-wrapper { + position: relative; + width: 110px; + height: 110px; + border-radius: 32% 68% 63% 37% / 34% 36% 64% 66%; + background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); + display: flex; + align-items: center; + justify-content: center; + border: 3px solid rgba(255, 255, 255, 0.15); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); + overflow: hidden; + } + + .studio-preview-image { + width: 100%; + height: 100%; + object-fit: cover; + } + + .studio-preview-placeholder { + font-size: 2.75rem; + font-weight: 800; + color: white; + } + + .preview-label { + font-size: 0.8rem; + color: var(--text-muted); + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + } + + .avatar-studio-controls { + flex: 1; + display: flex; + flex-direction: column; + gap: 1.25rem; + width: 100%; + } + + .control-section { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .row-controls { + display: flex; + flex-direction: row; + gap: 1.5rem; + } + + @media (max-width: 520px) { + .row-controls { + flex-direction: column; + gap: 1rem; + } + } + + .sub-control { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .control-label { + font-size: 0.82rem; + font-weight: 700; + color: var(--text-secondary); + letter-spacing: 0.3px; + } + + .preset-avatars-grid { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + } + + .preset-btn { + width: 38px; + height: 38px; + border-radius: 50%; + background-size: cover; + background-position: center; + border: 2px solid rgba(255, 255, 255, 0.12); + cursor: pointer; + transition: all 0.2s ease; + padding: 0; + } + + .preset-btn:hover { + transform: scale(1.1); + border-color: var(--primary, #6366f1); + } + + .preset-btn.active { + border-color: var(--primary, #6366f1); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.3); + transform: scale(1.05); + } + + .suggestions-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(85px, 1fr)); + gap: 0.75rem; + width: 100%; + } + + .suggestion-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 12px; + padding: 0.5rem; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + outline: none; + } + + .suggestion-card:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(99, 102, 241, 0.3); + transform: translateY(-2px); + } + + .suggestion-card.active { + background: rgba(99, 102, 241, 0.08); + border-color: var(--primary, #6366f1); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25); + } + + .suggestion-card:disabled { + opacity: 0.4; + cursor: not-allowed; + } + + .suggestion-preview { + width: 48px; + height: 48px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .suggestion-preview img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .suggestion-placeholder { + font-size: 0.95rem; + font-weight: 700; + color: var(--text-muted); + } + + .suggestion-name { + font-size: 0.72rem; + font-weight: 600; + color: var(--text-secondary); + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + } + + .github-sync-control { + min-width: 140px; + } + + .btn-github-sync { + padding: 0.5rem 1rem; + font-size: 0.85rem; + font-weight: 700; + color: white; + background: #24292e; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + + .btn-github-sync:hover { + background: #2f363d; + border-color: rgba(99, 102, 241, 0.4); + transform: translateY(-1px); + } + + textarea { + resize: vertical; + } + + .textarea-footer { + display: flex; + justify-content: flex-end; + margin-top: -0.25rem; + } + + .char-count { + font-size: 0.75rem; + color: var(--text-muted); + } + + /* --- Links Section in Form --- */ + .links-section-form { + border-top: 1px solid rgba(255, 255, 255, 0.08); + padding-top: 1.75rem; + display: flex; + flex-direction: column; + gap: 1rem; + } + + .links-section-form h3 { + font-size: 1.25rem; + font-weight: 700; + } + + .link-builder-row { + display: flex; + gap: 0.75rem; + align-items: center; + flex-wrap: wrap; + } + + .link-builder-row input { + flex: 1; + min-width: 200px; + } + + .select-wrapper { + position: relative; + display: flex; + } + + .select-wrapper select { + appearance: none; + padding-right: 2.5rem; + cursor: pointer; + background: rgba(255, 255, 255, 0.04); + } + + .select-wrapper::after { + content: "β–Ό"; + font-size: 0.75rem; + color: var(--text-muted); + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + } + + .btn-add-link { + padding: 0.75rem 1.25rem; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: var(--radius); + font-weight: 600; + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + } + + .btn-add-link:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(99, 102, 241, 0.4); + transform: translateY(-1px); + } + + /* --- Links List Container --- */ + .links-list-container { + background: rgba(0, 0, 0, 0.12); + border: 1px dashed rgba(255, 255, 255, 0.06); + border-radius: var(--radius-xl); + padding: 1rem; + min-height: 80px; + display: flex; + flex-direction: column; + justify-content: center; + } + + .empty-links-state { + text-align: center; + color: var(--text-muted); + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + } + + .empty-icon { + font-size: 1.5rem; + opacity: 0.5; + } + + .links-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .link-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + border-radius: var(--radius); + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); + gap: 1rem; + } + + .link-item-info { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; + flex: 1; + } + + .link-platform-tag { + font-size: 0.8rem; + font-weight: 700; + padding: 0.25rem 0.65rem; + border-radius: 999px; + color: white; + box-shadow: 0 2px 5px rgba(0,0,0,0.15); + } + + .link-username-tag { + font-size: 0.9rem; + font-weight: 600; + color: var(--text-primary); + } + + .link-url-tag { + font-size: 0.82rem; + color: var(--text-muted); + max-width: 320px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .btn-delete-link { + background: transparent; + border: none; + cursor: pointer; + font-size: 1.1rem; + padding: 0.25rem; + opacity: 0.6; + transition: opacity 0.2s ease, transform 0.2s ease; + } + + .btn-delete-link:hover { + opacity: 1; + transform: scale(1.15); + } + + /* --- Form Alerts --- */ + .alert { + padding: 1rem; + border-radius: var(--radius); + font-size: 0.95rem; + font-weight: 500; + border-left: 4px solid; + margin-bottom: 1.5rem; + } + + .alert-error { + background: rgba(239, 68, 68, 0.06); + border: 1px solid rgba(239, 68, 68, 0.12); + border-left-color: #ef4444; + color: #fca5a5; + } + + .alert-success { + background: rgba(34, 197, 94, 0.06); + border: 1px solid rgba(34, 197, 94, 0.12); + border-left-color: #22c55e; + color: #86efac; + } + + /* --- Form Action Buttons --- */ + .form-submit-wrapper { + display: flex; + gap: 1rem; + align-items: center; + border-top: 1px solid rgba(255, 255, 255, 0.08); + padding-top: 1.75rem; + margin-top: 0.5rem; + } + + .btn-generate-card { + padding: 0.9rem 2rem; + font-weight: 700; + border: none; + cursor: pointer; + background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); + color: white; + border-radius: calc(var(--radius) * 1.15); + box-shadow: 0 10px 25px -10px rgba(99, 102, 241, 0.4); + transition: all 0.25s ease; + display: inline-flex; + align-items: center; + } + + .btn-generate-card:hover { + transform: translateY(-2px); + box-shadow: 0 15px 30px -10px rgba(99, 102, 241, 0.5); + } + + .btn-generate-card:active { + transform: translateY(0); + } + + .btn-primary { + background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); + color: white; + border: none; + box-shadow: 0 10px 25px -10px rgba(99, 102, 241, 0.4); + text-decoration: none; + padding: 0.92rem 1.75rem; + border-radius: calc(var(--radius) * 1.15); + font-weight: 700; + transition: all 0.25s ease; + display: inline-flex; + align-items: center; + } + + .btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 15px 30px -10px rgba(99, 102, 241, 0.5); + } + + /* Smooth animations */ + @keyframes pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.08); opacity: 0.85; } + } + .features { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); @@ -221,13 +1505,13 @@ } @media (max-width: 640px) { - .features { - display: grid; - grid-template-columns: 1fr; /* single column */ - gap: 16px; - padding: 0 12px; + .features { + display: grid; + grid-template-columns: 1fr; + gap: 16px; + padding: 0 12px; + } } -} .feature-card { padding: 2.4rem; @@ -236,17 +1520,15 @@ background: linear-gradient(180deg, rgba(15, 23, 42, 0.75), rgba(15, 23, 42, 0.5)); border: 1px solid rgba(255, 255, 255, 0.08); transition: transform 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease; + min-height: 140px; } - .feature-card { - min-height: 140px; - padding: 16px; -} -@media (max-width: 640px) { - .feature-card { - margin-bottom: 12px; + @media (max-width: 640px) { + .feature-card { + margin-bottom: 12px; + padding: 1.8rem; + } } -} .feature-card:hover { transform: translateY(-8px); @@ -297,10 +1579,6 @@ align-items: stretch; } - .feature-card { - padding: 1.8rem; - } - .features { gap: 1.2rem; } diff --git a/apps/web/src/routes/u/[username]/+page.server.ts b/apps/web/src/routes/u/[username]/+page.server.ts index 042acad..8882b45 100644 --- a/apps/web/src/routes/u/[username]/+page.server.ts +++ b/apps/web/src/routes/u/[username]/+page.server.ts @@ -1,16 +1,2 @@ -import type { PageServerLoad } from './$types'; - -const API_BASE = process.env.BACKEND_URL || 'http://localhost:3000'; - -export const load: PageServerLoad = async ({ params, fetch }) => { - try { - const res = await fetch(`${API_BASE}/api/u/${params.username}?source=web`); - if (!res.ok) { - return { profile: null, error: 'User not found' }; - } - const profile = await res.json(); - return { profile, error: null }; - } catch { - return { profile: null, error: 'Failed to load profile' }; - } -}; +// This server load is intentionally disabled. Profile data is loaded client-side from localStorage. +export const load = async () => ({ profile: null, error: null }); diff --git a/apps/web/src/routes/u/[username]/+page.svelte b/apps/web/src/routes/u/[username]/+page.svelte index bb23cca..c4318ef 100644 --- a/apps/web/src/routes/u/[username]/+page.svelte +++ b/apps/web/src/routes/u/[username]/+page.svelte @@ -1,26 +1,69 @@ @@ -62,19 +137,25 @@ {profile.displayName} | DevCard {:else} - User Not Found | DevCard + Card Not Found | DevCard {/if}
- {#if error || !profile} + {#if errorStatus}
πŸ˜•
-

Profile not found

+

Card not found

This DevCard has vanished into the digital void.

- Return Home + Create yours +
+ {:else if !profile} + +
+
⚑
+

Retrieving your DevCard...

{:else}
@@ -91,11 +172,9 @@

{profile.displayName}

- {#if profile.role} -
- {profile.role}{profile.company ? ` @ ${profile.company}` : ''} -
- {/if} +
+ @{profile.username} +
{#if profile.bio}

{profile.bio}

@@ -104,26 +183,39 @@ + + +
+ +
+

Share Your Profile

+

Scan to view or tap below to download QR as a high-quality PNG.

+ +
+

Verified Developer Profile

@@ -132,11 +224,11 @@
-

Want a card like this?

- Create your DevCard ⚑ -

@@ -268,6 +360,8 @@ animation: slideIn 0.5s ease-out forwards; animation-delay: var(--delay); opacity: 0; + text-decoration: none; + color: inherit; } .link-tile:hover, @@ -329,8 +423,75 @@ transform: translateX(5px); } + /* --- QR Section Styles --- */ + .qr-section { + margin-top: 2.25rem; + padding: 1.5rem; + border-radius: var(--radius-lg, 16px); + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; + flex-wrap: wrap; + } + + .qr-canvas { + border-radius: 12px; + background: white; + padding: 6px; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35); + width: 140px; + height: 140px; + } + + .qr-actions { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.45rem; + flex: 1; + min-width: 180px; + } + + .qr-title { + font-size: 1.1rem; + font-weight: 700; + color: var(--text-primary); + } + + .qr-hint { + font-size: 0.85rem; + color: var(--text-muted); + line-height: 1.45; + margin-bottom: 0.25rem; + } + + .btn-qr-download { + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: var(--radius); + background: rgba(255, 255, 255, 0.08); + color: var(--text-primary); + cursor: pointer; + font: inherit; + font-weight: 700; + padding: 0.55rem 1.1rem; + font-size: 0.9rem; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 0.4rem; + } + + .btn-qr-download:hover { + background: rgba(255, 255, 255, 0.15); + transform: translateY(-1px); + border-color: var(--accent); + } + .card-footer { - margin-top: 2.5rem; + margin-top: 2.25rem; padding-top: 1.75rem; border-top: 1px solid rgba(255,255,255,0.08); display: flex; @@ -349,16 +510,10 @@ } .get-your-own { - margin-top: 2rem; + margin-top: 2.25rem; text-align: center; } - .get-your-own p { - margin-bottom: 0.5rem; - font-size: 0.95rem; - color: var(--text-muted); - } - .profile-actions { display: flex; flex-wrap: wrap; @@ -367,26 +522,25 @@ gap: 0.75rem; } - .get-devcard-link { - font-weight: 700; - font-size: 1.05rem; - } - .copy-link-button { - border: 1px solid var(--border-glass); + border: 1px solid rgba(255, 255, 255, 0.12); border-radius: var(--radius); - background: rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.06); color: var(--text-primary); cursor: pointer; font: inherit; font-weight: 700; - padding: 0.65rem 1rem; + padding: 0.65rem 1.2rem; transition: all 0.2s ease; + text-decoration: none; + display: inline-flex; + align-items: center; } .copy-link-button:hover { - background: rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.14); transform: translateY(-1px); + border-color: var(--accent); } .copy-link-button:focus-visible { @@ -411,9 +565,63 @@ .error-glass { text-align: center; - padding: 3rem; + padding: 3.5rem clamp(1.5rem, 4vw, 3rem); border-radius: var(--radius-xl); width: min(100%, 520px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-shadow: 0 26px 60px -20px rgba(0, 0, 0, 0.55); + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(15, 23, 42, 0.96); + } + + .error-glass h1 { + font-size: 2rem; + margin-bottom: 0.75rem; + } + + .error-glass p { + color: var(--text-secondary); + margin-bottom: 1.75rem; + line-height: 1.5; + } + + .error-emoji { + font-size: 3.5rem; + margin-bottom: 1.25rem; + } + + .spinner { + font-size: 3rem; + margin-bottom: 1.25rem; + animation: rotate 1.5s linear infinite; + } + + @keyframes rotate { + 0% { transform: rotate(0deg) scale(1); } + 50% { transform: rotate(180deg) scale(1.1); } + 100% { transform: rotate(360deg) scale(1); } + } + + .btn-primary { + background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); + color: white; + border: none; + box-shadow: 0 10px 25px -10px rgba(99, 102, 241, 0.4); + text-decoration: none; + padding: 0.8rem 1.75rem; + border-radius: calc(var(--radius) * 1.15); + font-weight: 700; + transition: all 0.25s ease; + display: inline-flex; + align-items: center; + } + + .btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 15px 30px -10px rgba(99, 102, 241, 0.5); } @media (max-width: 720px) { @@ -421,6 +629,7 @@ .profile-header { margin-bottom: 2rem; } .avatar-wrapper { width: 108px; height: 108px; margin-bottom: 1.5rem; } .card-footer { flex-direction: column; align-items: flex-start; } + .qr-section { gap: 1rem; padding: 1.2rem; } } @media (max-width: 520px) { @@ -429,5 +638,7 @@ .link-tile { padding: 0.95rem; } .tile-content { margin-left: 0.9rem; } .card-footer { text-align: left; } + .qr-section { flex-direction: column; align-items: center; text-align: center; } + .qr-actions { align-items: center; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5badd09..05218f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -247,6 +247,9 @@ importers: '@devcard/shared': specifier: workspace:* version: link:../../packages/shared + qrcode: + specifier: ^1.5.4 + version: 1.5.4 devDependencies: '@sveltejs/adapter-auto': specifier: ^7.0.0 @@ -257,6 +260,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^6.2.4 version: 6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 svelte: specifier: ^5.51.0 version: 5.53.10