Skip to content

Commit 6ea494e

Browse files
GraysonCAdamsclaude
andcommitted
Add proper mobile keyboard types and hints to all input fields
- Email inputs: autoComplete="email", enterKeyHint="send" - Search inputs: type="search", enterKeyHint="search", disable autocorrect - Join code inputs: enterKeyHint="go", disable autocomplete/autocapitalize - Number inputs: inputMode="numeric", enterKeyHint="done" - Name inputs: autoComplete="off", enterKeyHint="next"/"done" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6002ae6 commit 6ea494e

13 files changed

Lines changed: 115 additions & 51 deletions

File tree

src/app/LandingClient.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,11 @@ export default function LandingClient() {
853853
placeholder="Enter invite code"
854854
className="input-glass flex-1 text-sm"
855855
maxLength={10}
856+
enterKeyHint="go"
857+
autoComplete="off"
858+
autoCapitalize="off"
859+
autoCorrect="off"
860+
spellCheck={false}
856861
/>
857862
<button
858863
onClick={handleJoinSubmit}
@@ -906,6 +911,8 @@ export default function LandingClient() {
906911
placeholder="Spotify username or email"
907912
className="input-glass w-full"
908913
onKeyDown={(e) => e.key === 'Enter' && handleSignInLookup()}
914+
autoComplete="username"
915+
enterKeyHint="go"
909916
/>
910917
<button
911918
onClick={handleSignInLookup}

src/app/circle/[circleId]/settings/CircleSettingsClient.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,8 @@ export default function CircleSettingsClient({ circle, isHost }: CircleSettingsC
316316
onChange={(e) => setEditName(e.target.value)}
317317
placeholder="My Circle"
318318
className="input-glass w-full"
319+
autoComplete="off"
320+
enterKeyHint="done"
319321
/>
320322
</div>
321323
</div>

src/app/circle/join/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ function JoinCircleContent() {
176176
}}
177177
placeholder="Enter invite code"
178178
className="input-glass flex-1"
179+
enterKeyHint="go"
180+
autoComplete="off"
181+
autoCapitalize="off"
182+
autoCorrect="off"
183+
spellCheck={false}
179184
/>
180185
<button
181186
onClick={() => lookupCode(code.trim())}

src/app/dashboard/DashboardClient.tsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -99,50 +99,49 @@ const VIBE_PLACEHOLDERS: Array<{
9999
name: string;
100100
desc: string;
101101
vibeLine: string;
102-
emoji: string;
103-
gradient: [string, string];
102+
imageUrl: string;
104103
}> = [
105104
{
106105
name: 'late night drives',
107106
desc: 'windows down, vibes up',
108107
vibeLine: 'moody R&B \u00b7 chill rap \u00b7 slow jams',
109-
emoji: '\ud83c\udf19',
110-
gradient: ['#1e1b4b', '#312e81'],
108+
imageUrl:
109+
'https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=112&h=112&fit=crop&auto=format',
111110
},
112111
{
113112
name: 'UNHINGED BANGERS',
114113
desc: 'no skips, only chaos',
115114
vibeLine: 'hyperpop \u00b7 hard drops \u00b7 EDM',
116-
emoji: '\ud83d\udd25',
117-
gradient: ['#7f1d1d', '#dc2626'],
115+
imageUrl:
116+
'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=112&h=112&fit=crop&auto=format',
118117
},
119118
{
120119
name: 'main character energy',
121120
desc: 'the soundtrack to your villain arc',
122121
vibeLine: 'indie pop \u00b7 alt \u00b7 cinematic',
123-
emoji: '\u2728',
124-
gradient: ['#0c4a6e', '#38bdf8'],
122+
imageUrl:
123+
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=112&h=112&fit=crop&auto=format',
125124
},
126125
{
127126
name: 'COZY GIRL AUTUMN',
128127
desc: 'candles lit, oversized hoodie on',
129128
vibeLine: 'soft folk \u00b7 acoustic \u00b7 bedroom pop',
130-
emoji: '\ud83c\udf42',
131-
gradient: ['#78350f', '#d97706'],
129+
imageUrl:
130+
'https://images.unsplash.com/photo-1508193638397-1c4234db14d8?w=112&h=112&fit=crop&auto=format',
132131
},
133132
{
134133
name: 'gym arc',
135134
desc: 'PR or ER, no in between',
136135
vibeLine: 'trap \u00b7 drill \u00b7 rage beats',
137-
emoji: '\ud83d\udcaa',
138-
gradient: ['#14532d', '#22c55e'],
136+
imageUrl:
137+
'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=112&h=112&fit=crop&auto=format',
139138
},
140139
{
141140
name: 'TOUCH GRASS',
142141
desc: 'for when the group chat goes outside',
143142
vibeLine: 'feel-good \u00b7 summer hits \u00b7 throwbacks',
144-
emoji: '\ud83c\udf3b',
145-
gradient: ['#164e63', '#38bdf8'],
143+
imageUrl:
144+
'https://images.unsplash.com/photo-1501854140801-50d01698950b?w=112&h=112&fit=crop&auto=format',
146145
},
147146
];
148147

@@ -424,21 +423,16 @@ export default function DashboardClient({
424423
<div className="px-4 pt-2 pb-6">
425424
<div className="relative overflow-hidden">
426425
{/* Decorative placeholder cards */}
427-
<div className="space-y-2 opacity-40 saturate-0 pointer-events-none">
428-
{VIBE_PLACEHOLDERS.slice(0, 3).map((vibe, i) => (
426+
<div className="space-y-2 opacity-40 pointer-events-none">
427+
{VIBE_PLACEHOLDERS.slice(0, 5).map((vibe, i) => (
429428
<div
430429
key={vibe.name}
431430
className="glass rounded-2xl p-3.5 w-full text-left"
432-
style={i >= 1 ? { filter: `blur(${i * 1.5}px)` } : undefined}
431+
style={i >= 2 ? { filter: `blur(${(i - 1) * 1.5}px)` } : undefined}
433432
>
434433
<div className="flex items-center gap-3.5">
435-
<div
436-
className="w-14 h-14 shrink-0 rounded-lg overflow-hidden flex items-center justify-center text-2xl"
437-
style={{
438-
background: `linear-gradient(135deg, ${vibe.gradient[0]}, ${vibe.gradient[1]})`,
439-
}}
440-
>
441-
{vibe.emoji}
434+
<div className="w-14 h-14 shrink-0 rounded-lg overflow-hidden">
435+
<img src={vibe.imageUrl} alt="" className="w-full h-full object-cover" />
442436
</div>
443437
<div className="flex-1 min-w-0">
444438
<p className="text-[15px] font-semibold truncate text-text-primary leading-tight">
@@ -610,6 +604,8 @@ export default function DashboardClient({
610604
onChange={(e) => setCreateName(e.target.value)}
611605
placeholder="My Swaplist"
612606
className="input-glass w-full"
607+
autoComplete="off"
608+
enterKeyHint="next"
613609
/>
614610
</div>
615611
</div>
@@ -666,12 +662,16 @@ export default function DashboardClient({
666662
<div className="relative">
667663
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-tertiary" />
668664
<input
669-
type="text"
665+
type="search"
670666
value={playlistFilter}
671667
onChange={(e) => setPlaylistFilter(e.target.value)}
672668
placeholder="Filter playlists..."
673669
className="w-full input-glass"
674670
style={{ paddingLeft: '2.5rem' }}
671+
enterKeyHint="search"
672+
autoComplete="off"
673+
autoCorrect="off"
674+
spellCheck={false}
675675
/>
676676
</div>
677677
</div>

src/app/playlist/[playlistId]/settings/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,14 @@ export default function PlaylistSettingsPage() {
131131
<input
132132
id="maxTracksPerUser"
133133
type="number"
134+
inputMode="numeric"
134135
min="1"
135136
max="50"
136137
value={maxTracksPerUser}
137138
onChange={(e) => setMaxTracksPerUser(e.target.value)}
138139
placeholder="Unlimited"
139140
className="input-glass w-full"
141+
enterKeyHint="done"
140142
/>
141143
<p className="text-sm text-text-tertiary mt-1.5">
142144
Limit how many active tracks each member can have. Leave blank for unlimited.
@@ -197,11 +199,13 @@ export default function PlaylistSettingsPage() {
197199
<input
198200
id="maxTrackAgeDays"
199201
type="number"
202+
inputMode="numeric"
200203
min={0}
201204
max={365}
202205
value={maxTrackAgeDays}
203206
onChange={(e) => setMaxTrackAgeDays(Number(e.target.value))}
204207
className="input-glass w-full"
208+
enterKeyHint="done"
205209
/>
206210
{maxTrackAgeDays > 0 && (
207211
<p className="text-sm text-text-tertiary mt-2">

src/app/playlist/join/JoinPlaylistClient.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ function JoinPlaylistContent() {
140140
onChange={(e) => setCode(e.target.value)}
141141
placeholder="Enter invite code"
142142
className="input-glass flex-1"
143+
enterKeyHint="go"
144+
autoComplete="off"
145+
autoCapitalize="off"
146+
autoCorrect="off"
147+
spellCheck={false}
143148
/>
144149
<button
145150
onClick={() => lookupCode(code)}

src/app/profile/ProfileClient.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,8 @@ function ProfileContent({ user, stats }: ProfileClientProps) {
436436
onChange={(e) => setEmailInput(e.target.value)}
437437
required
438438
autoFocus
439+
autoComplete="email"
440+
enterKeyHint="send"
439441
/>
440442
<button
441443
type="submit"

src/app/verify-email/VerifyEmailClient.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ export default function VerifyEmailClient({
204204
className="input-glass w-full"
205205
required
206206
autoFocus
207+
autoComplete="email"
208+
enterKeyHint="send"
207209
/>
208210

209211
{error && <p className="text-sm text-danger text-center">{error}</p>}

src/components/AddMemberWizard.tsx

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ExternalLink,
1212
AlertCircle,
1313
Link2,
14+
Share2,
1415
} from 'lucide-react';
1516
import { toast } from 'sonner';
1617
import GlassDrawer from '@/components/ui/glass-drawer';
@@ -72,6 +73,20 @@ export default function AddMemberWizard({
7273
});
7374
}
7475

76+
async function handleNativeShare() {
77+
const link = `${window.location.origin}/circle/join?code=${inviteCode}`;
78+
try {
79+
await navigator.share({
80+
title: 'Join my circle on Swapify',
81+
text: `Use code: ${inviteCode}`,
82+
url: link,
83+
});
84+
} catch {
85+
// User cancelled or share API unavailable — fall back to copy
86+
handleCopyShareLink();
87+
}
88+
}
89+
7590
async function handleSendEmail() {
7691
if (!email.trim()) return;
7792
setIsSending(true);
@@ -224,47 +239,53 @@ export default function AddMemberWizard({
224239
Send this code to your friend so they can join your circle in Swapify.
225240
</p>
226241

227-
{/* Invite code display */}
228-
<div className="glass rounded-xl p-5 text-center">
242+
{/* Invite code display — tap to copy */}
243+
<button
244+
type="button"
245+
onClick={handleCopy}
246+
className="glass rounded-xl px-5 py-4 w-full flex items-center justify-between gap-3 group active:scale-[0.98] transition-transform"
247+
>
229248
<code className="text-2xl font-mono font-bold text-text-primary tracking-[0.2em] select-all">
230249
{inviteCode}
231250
</code>
251+
<span className="shrink-0 text-text-secondary group-hover:text-text-primary transition-colors">
252+
{copied ? (
253+
<Check className="w-5 h-5 text-accent-green" />
254+
) : (
255+
<Copy className="w-5 h-5" />
256+
)}
257+
</span>
258+
</button>
259+
260+
{/* Share actions */}
261+
<div className="flex gap-2">
232262
<button
233263
type="button"
234-
onClick={handleCopy}
235-
className="mt-3 mx-auto btn-pill btn-pill-secondary flex items-center gap-2 text-sm"
264+
onClick={handleCopyShareLink}
265+
className="flex-1 flex items-center justify-center gap-1.5 rounded-lg py-2 text-sm text-text-secondary hover:text-text-primary hover:bg-white/5 transition-colors"
236266
>
237-
{copied ? (
267+
{linkCopied ? (
238268
<>
239-
<Check className="w-4 h-4 text-green-400" />
240-
Copied!
269+
<Check className="w-3.5 h-3.5 text-accent-green" />
270+
<span className="text-accent-green">Copied!</span>
241271
</>
242272
) : (
243273
<>
244-
<Copy className="w-4 h-4" />
245-
Copy Code
274+
<Link2 className="w-3.5 h-3.5" />
275+
Copy Link
246276
</>
247277
)}
248278
</button>
249-
<div className="mt-3 pt-3 border-t border-white/5">
279+
{typeof navigator !== 'undefined' && 'share' in navigator && (
250280
<button
251281
type="button"
252-
onClick={handleCopyShareLink}
253-
className="mx-auto btn-pill btn-pill-secondary flex items-center gap-2 text-sm"
282+
onClick={handleNativeShare}
283+
className="flex-1 flex items-center justify-center gap-1.5 rounded-lg py-2 text-sm text-text-secondary hover:text-text-primary hover:bg-white/5 transition-colors"
254284
>
255-
{linkCopied ? (
256-
<>
257-
<Check className="w-4 h-4 text-green-400" />
258-
Copied!
259-
</>
260-
) : (
261-
<>
262-
<Link2 className="w-4 h-4" />
263-
Copy Share Link
264-
</>
265-
)}
285+
<Share2 className="w-3.5 h-3.5" />
286+
Share
266287
</button>
267-
</div>
288+
)}
268289
</div>
269290

270291
{/* Or send by email */}
@@ -279,6 +300,8 @@ export default function AddMemberWizard({
279300
onChange={(e) => setEmail(e.target.value)}
280301
placeholder="friend@example.com"
281302
className="input-glass flex-1"
303+
autoComplete="email"
304+
enterKeyHint="send"
282305
/>
283306
<button
284307
type="button"

src/components/CircleSwitcher.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,9 @@ export default function CircleSwitcher({
188188
}}
189189
title="Your Circles"
190190
snapPoint="half"
191+
direction="top"
191192
>
192-
<div className="flex flex-col gap-2">
193+
<div className="flex flex-col gap-2 py-1">
193194
{circles.map((circle, index) => {
194195
const isActive = circle.id === activeCircleId;
195196
const isLoading = isSwitching === circle.id;
@@ -394,6 +395,11 @@ export default function CircleSwitcher({
394395
}}
395396
placeholder="Enter invite code"
396397
className="input-glass flex-1"
398+
enterKeyHint="go"
399+
autoComplete="off"
400+
autoCapitalize="off"
401+
autoCorrect="off"
402+
spellCheck={false}
397403
/>
398404
<button
399405
onClick={() => lookupCircleCode(joinCode.trim())}

0 commit comments

Comments
 (0)