feat: UX improvements - credits, draggable sasquatch, mobile fixes
1. AboutPage: add Credits section (Zapstore, ContextVM, Angor, Geyser, MKStack, Shakespeare, Nostr), add Zapstore download button, fix powered-by text overflow on mobile (wrap-friendly flex layout) 2. SasquatchGuide: make minimized bubble draggable on all devices - pointer events for mouse, touch, and stylus - saves position to localStorage, persists across sessions - tap still opens, drag moves (5px threshold to distinguish) 3. Layout (burger menu): add overflow-y-auto + pb-8 to SheetContent so mobile menu is scrollable when content exceeds screen height 4. CashuWalletCard (wallet): fix input+button rows overflowing on mobile - flex-col on small screens, flex-row on xs+ (stacked on phones) - whitespace-nowrap on buttons to prevent text wrapping 5. LabPage: compact action buttons on mobile (h-8, text-[10px], px-2) so all 4 buttons fit in 2x2 grid without overflow/truncation 6. LoginDialog (Amber): improve visibility-change handler - use fresh SimplePool subscription on each foreground return instead of querySync (which doesn't exist in nostr-tools v2.13) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
84d80ac3a7
commit
5dffa5cac3
6 changed files with 231 additions and 108 deletions
|
|
@ -571,8 +571,8 @@ export function Layout({ children }: LayoutProps) {
|
||||||
<Menu className="h-5 w-5" />
|
<Menu className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="right" className="w-80">
|
<SheetContent side="right" className="w-80 overflow-y-auto">
|
||||||
<div className="flex flex-col gap-6 mt-6">
|
<div className="flex flex-col gap-6 mt-6 pb-8">
|
||||||
{/* Wallet Card - Mobile Menu */}
|
{/* Wallet Card - Mobile Menu */}
|
||||||
{user && (
|
{user && (
|
||||||
<Link
|
<Link
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* Falls back to static FAQ if AI is unavailable.
|
* Falls back to static FAQ if AI is unavailable.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
|
@ -322,6 +322,53 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
|
||||||
const [streamingText, setStreamingText] = useState('');
|
const [streamingText, setStreamingText] = useState('');
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Draggable position state for minimized bubble
|
||||||
|
const [dragPos, setDragPos] = useLocalStorage<{ x: number; y: number } | null>('sasquatch-pos', null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const dragRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dragStart = useRef<{ mx: number; my: number; bx: number; by: number } | null>(null);
|
||||||
|
const dragMoved = useRef(false);
|
||||||
|
|
||||||
|
// Dragging handlers
|
||||||
|
const onPointerDown = useCallback((e: React.PointerEvent) => {
|
||||||
|
if (!dragRef.current) return;
|
||||||
|
dragMoved.current = false;
|
||||||
|
const rect = dragRef.current.getBoundingClientRect();
|
||||||
|
dragStart.current = { mx: e.clientX, my: e.clientY, bx: rect.left, by: rect.top };
|
||||||
|
dragRef.current.setPointerCapture(e.pointerId);
|
||||||
|
setIsDragging(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onPointerMove = useCallback((e: React.PointerEvent) => {
|
||||||
|
if (!dragStart.current) return;
|
||||||
|
const dx = e.clientX - dragStart.current.mx;
|
||||||
|
const dy = e.clientY - dragStart.current.my;
|
||||||
|
if (!dragMoved.current && Math.abs(dx) < 5 && Math.abs(dy) < 5) return;
|
||||||
|
dragMoved.current = true;
|
||||||
|
setIsDragging(true);
|
||||||
|
const newX = Math.max(0, Math.min(window.innerWidth - 56, dragStart.current.bx + dx));
|
||||||
|
const newY = Math.max(0, Math.min(window.innerHeight - 56, dragStart.current.by + dy));
|
||||||
|
setDragPos({ x: newX, y: newY });
|
||||||
|
}, [setDragPos]);
|
||||||
|
|
||||||
|
const onPointerUp = useCallback((e: React.PointerEvent) => {
|
||||||
|
if (dragRef.current) dragRef.current.releasePointerCapture(e.pointerId);
|
||||||
|
const wasDrag = dragMoved.current;
|
||||||
|
dragStart.current = null;
|
||||||
|
dragMoved.current = false;
|
||||||
|
setTimeout(() => setIsDragging(false), 50);
|
||||||
|
if (!wasDrag) {
|
||||||
|
setIsMinimized(false);
|
||||||
|
}
|
||||||
|
}, [setIsMinimized]);
|
||||||
|
|
||||||
|
const bubbleStyle = useMemo(() => {
|
||||||
|
if (dragPos) {
|
||||||
|
return { position: 'fixed' as const, left: dragPos.x, top: dragPos.y, bottom: 'auto', right: 'auto', zIndex: 50 };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [dragPos]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (message) {
|
if (message) {
|
||||||
const timer = setTimeout(() => setIsVisible(true), 500);
|
const timer = setTimeout(() => setIsVisible(true), 500);
|
||||||
|
|
@ -501,12 +548,25 @@ Instructions:
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ── Minimized bubble — always visible, bottom-left ── */}
|
{/* ── Minimized bubble — draggable ── */}
|
||||||
{isMinimized && (
|
{isMinimized && (
|
||||||
<div className="fixed bottom-20 md:bottom-6 left-3 md:left-4 z-50">
|
<div
|
||||||
<button
|
ref={dragRef}
|
||||||
onClick={() => setIsMinimized(false)}
|
className={cn(
|
||||||
className="relative group flex items-center justify-center w-12 h-12 md:w-14 md:h-14 rounded-full shadow-2xl border-2 border-[#ccff00]/60 hover:border-[#ccff00] transition-all duration-200 hover:scale-110 active:scale-95"
|
'z-50 touch-none select-none',
|
||||||
|
dragPos ? '' : 'fixed bottom-20 md:bottom-6 left-3 md:left-4',
|
||||||
|
)}
|
||||||
|
style={bubbleStyle}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
title="Press and drag to move · Tap to open"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative group flex items-center justify-center w-12 h-12 md:w-14 md:h-14 rounded-full shadow-2xl border-2 border-[#ccff00]/60 transition-all duration-200',
|
||||||
|
isDragging ? 'scale-110 border-[#ccff00] cursor-grabbing' : 'hover:border-[#ccff00] hover:scale-110 active:scale-95 cursor-grab',
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
background: 'radial-gradient(circle at 40% 35%, #2d4a1e 0%, #1a2e0f 50%, #0d1a07 100%)',
|
background: 'radial-gradient(circle at 40% 35%, #2d4a1e 0%, #1a2e0f 50%, #0d1a07 100%)',
|
||||||
boxShadow: '0 0 20px rgba(204,255,0,0.2), 0 4px 20px rgba(0,0,0,0.8)',
|
boxShadow: '0 0 20px rgba(204,255,0,0.2), 0 4px 20px rgba(0,0,0,0.8)',
|
||||||
|
|
@ -516,13 +576,11 @@ Instructions:
|
||||||
<img
|
<img
|
||||||
src="/images/sasquatch-guide.webp"
|
src="/images/sasquatch-guide.webp"
|
||||||
alt="The Sasquatch"
|
alt="The Sasquatch"
|
||||||
className="w-8 h-8 md:w-9 md:h-9 rounded-full object-cover"
|
className="w-8 h-8 md:w-9 md:h-9 rounded-full object-cover pointer-events-none"
|
||||||
/>
|
/>
|
||||||
{/* Neon pulse ring */}
|
|
||||||
<span className="absolute inset-0 rounded-full animate-ping opacity-20 border-2 border-[#ccff00]" />
|
<span className="absolute inset-0 rounded-full animate-ping opacity-20 border-2 border-[#ccff00]" />
|
||||||
{/* Live dot */}
|
|
||||||
<span className="absolute top-0 right-0 w-3 h-3 bg-[#ccff00] rounded-full border-2 border-black shadow-[0_0_6px_#ccff00]" />
|
<span className="absolute top-0 right-0 w-3 h-3 bg-[#ccff00] rounded-full border-2 border-black shadow-[0_0_6px_#ccff00]" />
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,49 +157,54 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
const pending = pendingRef.current || loadPendingNC();
|
const pending = pendingRef.current || loadPendingNC();
|
||||||
if (!pending || amberStep !== 'waiting') return;
|
if (!pending || amberStep !== 'waiting') return;
|
||||||
|
|
||||||
setAmberStatus('Checking relay for Amber response...');
|
setAmberStatus('Checking for Amber response...');
|
||||||
|
|
||||||
// Quick one-shot relay check when app comes back to foreground
|
|
||||||
const { nsecDecode } = await import('nostr-tools/nip19').catch(() => ({ nsecDecode: null }));
|
|
||||||
if (!nsecDecode) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded = nip19.decode(pending.clientNsec);
|
const decoded = nip19.decode(pending.clientNsec);
|
||||||
if (decoded.type !== 'nsec') return;
|
if (decoded.type !== 'nsec') return;
|
||||||
const clientSk = decoded.data;
|
const clientSk = decoded.data;
|
||||||
|
|
||||||
const pool = new SimplePool();
|
|
||||||
const since = Math.floor(pending.timestamp / 1000) - 30;
|
const since = Math.floor(pending.timestamp / 1000) - 30;
|
||||||
|
|
||||||
// Fetch recent events — one-shot query
|
// Re-subscribe with a fresh pool each time app comes to foreground
|
||||||
const events = await pool.querySync(
|
const freshPool = new SimplePool();
|
||||||
NOSTRCONNECT_RELAYS,
|
let found = false;
|
||||||
{
|
|
||||||
kinds: [24133],
|
|
||||||
'#p': [pending.clientPubkey],
|
|
||||||
since,
|
|
||||||
},
|
|
||||||
{ maxWait: 5000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
pool.close(NOSTRCONNECT_RELAYS);
|
await new Promise<void>((resolve) => {
|
||||||
|
const timeout = setTimeout(() => { resolve(); }, 6000);
|
||||||
|
|
||||||
for (const event of events) {
|
const sub = freshPool.subscribeMany(
|
||||||
try {
|
NOSTRCONNECT_RELAYS,
|
||||||
const decrypted = await nip04.decrypt(clientSk, event.pubkey, event.content);
|
[{ kinds: [24133], '#p': [pending.clientPubkey], since }],
|
||||||
const parsed = JSON.parse(decrypted) as { id: string; result: string; error?: string };
|
{
|
||||||
if (parsed.error && parsed.error !== '') continue;
|
onevent: async (event) => {
|
||||||
if (parsed.result !== pending.secret && parsed.result !== 'ack') continue;
|
if (found) return;
|
||||||
|
try {
|
||||||
|
const decrypted = await nip04.decrypt(clientSk, event.pubkey, event.content);
|
||||||
|
const parsed = JSON.parse(decrypted) as { id: string; result: string; error?: string };
|
||||||
|
if (parsed.error && parsed.error !== '') return;
|
||||||
|
if (parsed.result !== pending.secret && parsed.result !== 'ack') return;
|
||||||
|
found = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
sub.close();
|
||||||
|
freshPool.close(NOSTRCONNECT_RELAYS);
|
||||||
|
pendingRef.current = null;
|
||||||
|
abortRef.current?.abort();
|
||||||
|
await completeLogin(event.pubkey, clientSk, pending.clientNsec, pending.relays);
|
||||||
|
resolve();
|
||||||
|
} catch { /* wrong encryption */ }
|
||||||
|
},
|
||||||
|
oneose: () => {
|
||||||
|
// All caught up — wait a bit more then give up
|
||||||
|
setTimeout(() => { sub.close(); freshPool.close(NOSTRCONNECT_RELAYS); resolve(); }, 2000);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Found it!
|
if (!found) {
|
||||||
pendingRef.current = null;
|
setAmberStatus('Not received yet. Did you approve in Amber? Tap retry or come back after approving.');
|
||||||
abortRef.current?.abort();
|
|
||||||
await completeLogin(event.pubkey, clientSk, pending.clientNsec, pending.relays);
|
|
||||||
return;
|
|
||||||
} catch { /* wrong encryption or format */ }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setAmberStatus('No response yet — did you approve in Amber?');
|
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -294,24 +294,25 @@ export function CashuWalletCard() {
|
||||||
{/* Receive Tab */}
|
{/* Receive Tab */}
|
||||||
<TabsContent value="receive" className="space-y-4 mt-4">
|
<TabsContent value="receive" className="space-y-4 mt-4">
|
||||||
{/* Lightning Invoice */}
|
{/* Lightning Invoice */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="text-sm font-medium">Receive Lightning Payment</Label>
|
<Label className="text-sm font-medium">Receive Lightning Payment</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-col xs:flex-row gap-2">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Amount in sats"
|
placeholder="Amount in sats"
|
||||||
value={quoteAmount}
|
value={quoteAmount}
|
||||||
onChange={(e) => setQuoteAmount(e.target.value)}
|
onChange={(e) => setQuoteAmount(e.target.value)}
|
||||||
/>
|
className="min-w-0"
|
||||||
<Button
|
/>
|
||||||
onClick={handleLightningReceive}
|
<Button
|
||||||
disabled={!activeMintUrl || !quoteAmount}
|
onClick={handleLightningReceive}
|
||||||
className="bg-orange-500 hover:bg-orange-600"
|
disabled={!activeMintUrl || !quoteAmount}
|
||||||
>
|
className="bg-orange-500 hover:bg-orange-600 whitespace-nowrap xs:flex-shrink-0"
|
||||||
Generate Invoice
|
>
|
||||||
</Button>
|
Generate Invoice
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
|
@ -357,24 +358,25 @@ export function CashuWalletCard() {
|
||||||
{/* Send Tab */}
|
{/* Send Tab */}
|
||||||
<TabsContent value="send" className="space-y-4 mt-4">
|
<TabsContent value="send" className="space-y-4 mt-4">
|
||||||
{/* Send Token */}
|
{/* Send Token */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="text-sm font-medium">Create Cashu Token</Label>
|
<Label className="text-sm font-medium">Create Cashu Token</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-col xs:flex-row gap-2">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Amount in sats"
|
placeholder="Amount in sats"
|
||||||
value={sendAmount}
|
value={sendAmount}
|
||||||
onChange={(e) => setSendAmount(e.target.value)}
|
onChange={(e) => setSendAmount(e.target.value)}
|
||||||
/>
|
className="min-w-0"
|
||||||
<Button
|
/>
|
||||||
onClick={handleTokenSend}
|
<Button
|
||||||
disabled={!activeMintUrl || !sendAmount}
|
onClick={handleTokenSend}
|
||||||
className="bg-orange-500 hover:bg-orange-600"
|
disabled={!activeMintUrl || !sendAmount}
|
||||||
>
|
className="bg-orange-500 hover:bg-orange-600 whitespace-nowrap xs:flex-shrink-0"
|
||||||
Create Token
|
>
|
||||||
</Button>
|
Create Token
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import {
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
Download,
|
||||||
|
Smartphone,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
|
|
@ -235,26 +237,82 @@ Version: v0.1.0`;
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Download on Zapstore */}
|
||||||
|
<Card className="bg-black/90 border-2 border-[#ccff00]/50 mt-8">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||||
|
<div className="flex-1 text-center sm:text-left">
|
||||||
|
<div className="flex items-center gap-2 justify-center sm:justify-start mb-2">
|
||||||
|
<Smartphone className="h-5 w-5 text-[#ccff00]" />
|
||||||
|
<h3 className="font-black uppercase text-[#ccff00]">Download the App</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-300">
|
||||||
|
Install SkateSpots on Android via Zapstore — the open-source, Bitcoin-native app store.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://zapstore.dev/apps/org.skatespots.app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<Button className="gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase px-6">
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
Get on Zapstore
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Credits */}
|
||||||
|
<Card className="bg-black/90 border-2 border-gray-700 mt-8">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<h3 className="font-black uppercase text-white mb-4 flex items-center gap-2">
|
||||||
|
<Heart className="h-5 w-5 text-[#ff0040]" />
|
||||||
|
Credits
|
||||||
|
</h3>
|
||||||
|
<div className="grid sm:grid-cols-2 gap-3 text-sm">
|
||||||
|
{[
|
||||||
|
{ label: 'Built with', name: 'MKStack', url: 'https://soapbox.pub/mkstack', desc: 'Nostr client framework' },
|
||||||
|
{ label: 'Vibed with', name: 'Shakespeare', url: 'https://shakespeare.diy', desc: 'AI-powered development' },
|
||||||
|
{ label: 'Powered by', name: 'Nostr', url: 'https://nostr.com', desc: 'Decentralized protocol' },
|
||||||
|
{ label: 'Crowdfunding', name: 'Geyser', url: 'https://geyser.fund/project/skatespots', desc: 'Bitcoin Lightning crowdfunding' },
|
||||||
|
{ label: 'Crowdfunding', name: 'Angor', url: 'https://angor.io/', desc: 'Bitcoin crowdfunding platform' },
|
||||||
|
{ label: 'Distributed on', name: 'Zapstore', url: 'https://zapstore.dev/apps/org.skatespots.app', desc: 'Open-source app store' },
|
||||||
|
{ label: 'AI powered by', name: 'ContextVM', url: 'https://contextvm.org/s/e3564bb877adb0c99a7e4973ccb22e516d7d9eca0027d0bd62a04174ed670530', desc: 'Nostr AI infrastructure' },
|
||||||
|
].map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.name}
|
||||||
|
href={item.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-3 p-3 rounded-lg bg-black/50 border border-gray-800 hover:border-[#ccff00]/40 hover:bg-[#ccff00]/5 transition-all group"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4 text-gray-600 group-hover:text-[#ccff00] flex-shrink-0" />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<span className="text-gray-500 text-xs">{item.label}: </span>
|
||||||
|
<span className="font-bold text-white group-hover:text-[#ccff00] transition-colors">{item.name}</span>
|
||||||
|
<p className="text-xs text-gray-500 truncate">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Powered By */}
|
{/* Powered By */}
|
||||||
<div className="text-center mt-12 p-6">
|
<div className="text-center mt-8 pb-6 px-4">
|
||||||
<p className="text-gray-400 mb-2 uppercase tracking-wide">Powered by</p>
|
<p className="text-gray-400 mb-3 uppercase tracking-wide text-sm">Powered by</p>
|
||||||
<div className="flex items-center justify-center gap-3">
|
<div className="flex flex-wrap items-center justify-center gap-2 md:gap-3">
|
||||||
<a
|
<a href="https://nostr.com" target="_blank" rel="noopener noreferrer"
|
||||||
href="https://nostr.com"
|
className="text-[#ccff00] hover:text-[#ccff00]/80 font-black text-lg md:text-xl uppercase">
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-[#ccff00] hover:text-[#ccff00]/80 font-black text-xl uppercase"
|
|
||||||
>
|
|
||||||
Nostr
|
Nostr
|
||||||
</a>
|
</a>
|
||||||
<span className="text-gray-600">•</span>
|
<span className="text-gray-600">•</span>
|
||||||
<span className="text-[#ff0040] font-black text-xl uppercase">
|
<span className="text-[#ff0040] font-black text-lg md:text-xl uppercase">Decentralized</span>
|
||||||
Decentralized
|
|
||||||
</span>
|
|
||||||
<span className="text-gray-600">•</span>
|
<span className="text-gray-600">•</span>
|
||||||
<span className="text-orange-400 font-black text-xl uppercase">
|
<span className="text-orange-400 font-black text-lg md:text-xl uppercase">Community-Owned</span>
|
||||||
Community-Owned
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -111,29 +111,29 @@ export default function LabPage() {
|
||||||
<p className="text-gray-400">{t('labPage.subtitle')}</p>
|
<p className="text-gray-400">{t('labPage.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="grid grid-cols-2 sm:flex sm:flex-wrap gap-2 w-full md:w-auto">
|
<div className="grid grid-cols-2 gap-1.5 w-full md:w-auto md:flex md:flex-wrap md:gap-2">
|
||||||
<Link to="/become-coach" className="col-span-1">
|
<Link to="/become-coach">
|
||||||
<Button className="w-full bg-red-500 hover:bg-red-600 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
<Button size="sm" className="w-full bg-red-500 hover:bg-red-600 gap-1 uppercase font-black text-[10px] sm:text-xs h-8 px-2">
|
||||||
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
<Plus className="h-3 w-3 flex-shrink-0" />
|
||||||
<span className="truncate">{t('labPage.offerCoaching')}</span>
|
<span className="truncate">{t('labPage.offerCoaching')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/become-filmer" className="col-span-1">
|
<Link to="/become-filmer">
|
||||||
<Button className="w-full bg-orange-500 hover:bg-orange-600 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
<Button size="sm" className="w-full bg-orange-500 hover:bg-orange-600 gap-1 uppercase font-black text-[10px] sm:text-xs h-8 px-2">
|
||||||
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
<Plus className="h-3 w-3 flex-shrink-0" />
|
||||||
<span className="truncate">{t('labPage.offerFilming')}</span>
|
<span className="truncate">{t('labPage.offerFilming')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/become-editor" className="col-span-1">
|
<Link to="/become-editor">
|
||||||
<Button className="w-full bg-purple-600 hover:bg-purple-700 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
<Button size="sm" className="w-full bg-purple-600 hover:bg-purple-700 gap-1 uppercase font-black text-[10px] sm:text-xs h-8 px-2">
|
||||||
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
<Plus className="h-3 w-3 flex-shrink-0" />
|
||||||
<span className="truncate">Offer Editing</span>
|
<span className="truncate">Offer Editing</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/card" className="col-span-1">
|
<Link to="/card">
|
||||||
<Button variant="outline" className="w-full gap-1.5 uppercase font-black border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10 text-xs sm:text-sm">
|
<Button size="sm" variant="outline" className="w-full gap-1 uppercase font-black border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10 text-[10px] sm:text-xs h-8 px-2">
|
||||||
<CreditCard className="h-3.5 w-3.5 flex-shrink-0" />
|
<CreditCard className="h-3 w-3 flex-shrink-0" />
|
||||||
<span className="truncate">Get Your Card</span>
|
<span className="truncate">Get Card</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue