Compare commits
10 commits
74bd140295
...
2dcf5a2aad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dcf5a2aad | ||
|
|
50c95d0ff7 | ||
|
|
4ff736734d | ||
|
|
fd1713f6b1 | ||
|
|
6c9b6fab02 | ||
|
|
0ae4c59630 | ||
|
|
ab11b0d3ed | ||
|
|
0355c10dbc | ||
|
|
28d34e3fbe | ||
|
|
b0df599b52 |
11 changed files with 176 additions and 55 deletions
19
AGENTS.md
19
AGENTS.md
|
|
@ -2228,7 +2228,7 @@ session with **no other memory of this project**. So:
|
|||
instead of showing the error boundary.
|
||||
- **Baselines** (verified 2026-10-29): `npm test` → **0 errors, 0 warnings**,
|
||||
**156 tests**, build ok. The gate also type-checks the CVM server
|
||||
(`tsc -p cvm-server/tsconfig.json`). `package.json` version **1.7.57**.
|
||||
(`tsc -p cvm-server/tsconfig.json`). `package.json` version **1.7.61**.
|
||||
- ✅ Lint is **clean**: `no-explicit-any` = 0 · `react-hooks/exhaustive-deps` = 0
|
||||
· `no-unused-vars` = 0 · `react-refresh` = 0. Non-component exports were
|
||||
**split into their own modules** (`contexts/language.ts`, `lib/sortOptions.ts`,
|
||||
|
|
@ -2785,6 +2785,23 @@ Rules:
|
|||
profile; the adjacent chevron opens the account menu. Removed the
|
||||
redundant “IN” label so the header fits at 390px. APK sha256 `846efb24…`,
|
||||
versionCode 10757. Web live at `5026b63`.
|
||||
- **1.7.58 (2026-10-30):** the top-left SkateSpots brand lockup opens the
|
||||
signed-in user’s profile on mobile and desktop; signed out it still opens
|
||||
the map. Bottom Spots / desktop Map remains the home shortcut. APK sha256
|
||||
`0d7bdad7…`, versionCode 10758. Web live at `74bd140`.
|
||||
- **1.7.59 (2026-10-30):** owner/admin Delete in spot/shop card menus; mobile
|
||||
Back arrow on every non-map screen (history fallback to map); avatar account
|
||||
menu trimmed to status/profile/settings/accounts/logout; brand wordmark
|
||||
hidden on sub-pages for narrow-header fit. APK sha256 `0fd4a149…`,
|
||||
versionCode 10759. Web live at `0355c10`.
|
||||
- **1.7.60 (2026-10-30):** Cordn header shows last successful check; sellers
|
||||
see payment-readiness in Create Listing + offers; uploads resize photos to
|
||||
1600px WebP when smaller (GIF/undecodable formats untouched), cutting
|
||||
quota/storage/upload time. APK sha256 `8f11c229…`, versionCode 10760.
|
||||
Web live at `6c9b6fa`.
|
||||
- **1.7.61 (2026-10-30):** Cordn setup timeline now checks prepared device
|
||||
/sent request and spins on the approval step while waiting. APK sha256
|
||||
`db0279f7…`, versionCode 10761. Web live at `50c95d0`.
|
||||
|
||||
### Relay archive (dataset backup)
|
||||
- **Transfer content between keys (`scripts/transfer-content.mjs`, 2026-10-04):**
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"version": "1.7.58",
|
||||
"version": "1.7.61",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "skate-spots",
|
||||
"version": "1.7.58",
|
||||
"version": "1.7.61",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@bitcoinerlab/secp256k1": "^1.2.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"private": true,
|
||||
"version": "1.7.58",
|
||||
"version": "1.7.61",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"homepage": "https://skate-spots.org",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,35 @@
|
|||
🧩 Cordn setup is easier to follow
|
||||
- The setup steps now show progress: preparing this device is checked once the
|
||||
key package is ready, and the approval step shows a spinner while waiting.
|
||||
- The private-chat header already shows the last successful check, so “live” and
|
||||
“still waiting” are distinguishable at a glance.
|
||||
|
||||
---
|
||||
|
||||
⚡ Cordn status, payment readiness, lighter photos
|
||||
- Cordn panels now show the last successful check next to the connection status,
|
||||
so “waiting” and “live” are easier to tell apart.
|
||||
- Sellers see a payment-readiness card in listing creation and on incoming
|
||||
offers, with a direct link to add a Lightning address before accepting a deal.
|
||||
- Photos are resized to max 1600px and converted to WebP before upload when that
|
||||
makes them smaller. GIFs and formats the WebView cannot decode are left alone.
|
||||
This cuts upload time, quota use and Blossom storage.
|
||||
|
||||
---
|
||||
|
||||
🧹 Spot owners can delete from the card menu
|
||||
- Owner/admin members now get a **Delete** action in the three-dot menu on
|
||||
spot and shop cards, opening the existing confirmation dialog.
|
||||
- Added a mobile **Back** arrow on every non-map screen, with history fallback
|
||||
to the map. The bottom Spots tab remains the home shortcut.
|
||||
- Trimmed the avatar account menu to purpose-built actions: status, profile,
|
||||
settings, account switching and logout. The larger navigation stays in the
|
||||
burger menu.
|
||||
- Brand wordmark hides on sub-pages so the Back arrow and header controls fit
|
||||
on narrow phones without truncation.
|
||||
|
||||
---
|
||||
|
||||
🧭 Brand logo opens your profile
|
||||
- The top-left SkateSpots lockup now opens your public profile when signed in
|
||||
(both mobile and desktop). Signed out, it still goes to the map.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useRef, useEffect, lazy, Suspense } from 'react';
|
||||
import { Link, Outlet, useLocation } from 'react-router-dom';
|
||||
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isNativePlatform } from '@/lib/platform';
|
||||
|
|
@ -39,7 +39,6 @@ import {
|
|||
User,
|
||||
Plus,
|
||||
CheckCircle2,
|
||||
Database,
|
||||
Package,
|
||||
Heart,
|
||||
CreditCard,
|
||||
|
|
@ -53,6 +52,7 @@ import {
|
|||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
ArrowLeft,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import { OfflineIndicator } from '@/components/OfflineIndicator';
|
||||
|
|
@ -283,12 +283,6 @@ function MobileHeaderUserMenu({
|
|||
<span className="text-green-400 font-bold">{t('chromeUi.setStatus')}</span>
|
||||
<span className="ml-auto text-xs text-green-500">🛹</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/create-spot" className="flex items-center gap-2 cursor-pointer">
|
||||
<Plus className="h-4 w-4 text-[#ccff00]" />
|
||||
<span className="text-[#ccff00] font-medium">{t('chromeUi.addSpot')}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to={`/${nip19.npubEncode(pubkey)}`} className="flex items-center gap-2 cursor-pointer">
|
||||
<User className="h-4 w-4" />
|
||||
|
|
@ -301,36 +295,6 @@ function MobileHeaderUserMenu({
|
|||
{t('nav.settings')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings?section=mydata" className="flex items-center gap-2 cursor-pointer">
|
||||
<Database className="h-4 w-4 text-[#ccff00]" />
|
||||
<span className="text-[#ccff00]">{t('chromeUi.myData')}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/stash" className="flex items-center gap-2 cursor-pointer">
|
||||
<Package className="h-4 w-4 text-purple-400" />
|
||||
<span className="text-purple-400">{t('chromeUi.myStash')}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/wallet" className="flex items-center gap-2 cursor-pointer">
|
||||
<Wallet className="h-4 w-4 text-orange-400" />
|
||||
<span className="text-orange-400">{t('nav.wallet')}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/leaderboards" className="flex items-center gap-2 cursor-pointer">
|
||||
<BarChart3 className="h-4 w-4 text-[#ccff00]" />
|
||||
<span className="text-[#ccff00]">{t('nav.leaderboards')}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/marketplace" className="flex items-center gap-2 cursor-pointer">
|
||||
<Package className="h-4 w-4 text-orange-400" />
|
||||
<span className="text-orange-400">{t('nav.marketplace')}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* ── Accounts: switch between logged-in npubs + add another ── */}
|
||||
<DropdownMenuSeparator />
|
||||
|
|
@ -512,6 +476,8 @@ function MobileNav({
|
|||
|
||||
export function Layout({ children, noPadding }: LayoutProps) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const showMobileBack = location.pathname !== '/' && location.pathname !== '/map';
|
||||
// The map is full-bleed (it manages its own height), so it gets no bottom
|
||||
// padding even though it shares this one persistent layout with every route.
|
||||
const isFullBleed = noPadding || location.pathname === '/' || location.pathname === '/map';
|
||||
|
|
@ -842,6 +808,22 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
{/* Mobile Header */}
|
||||
<header className="md:hidden sticky top-0 z-50 border-b border-red-500/30 bg-black/95 backdrop-blur safe-area-top">
|
||||
<div className="flex items-center justify-between h-14 px-3 gap-3">
|
||||
{showMobileBack && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="-ml-1 h-9 w-9 shrink-0 text-gray-300 hover:bg-white/5 hover:text-white"
|
||||
aria-label={t('common.back')}
|
||||
title={t('common.back')}
|
||||
onClick={() => {
|
||||
if (location.key !== 'default') navigate(-1);
|
||||
else navigate('/');
|
||||
}}
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
<Link
|
||||
to={user ? `/${nip19.npubEncode(user.pubkey)}` : '/'}
|
||||
className="flex items-center gap-2 touch-manipulation active:opacity-80 flex-1 min-w-0"
|
||||
|
|
@ -853,7 +835,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
alt="SkateSpots"
|
||||
className="w-9 h-9 rounded-lg flex-shrink-0 shadow-lg shadow-red-500/30"
|
||||
/>
|
||||
<span className="min-w-0 truncate font-display text-lg neon-red tracking-wide" style={{
|
||||
<span className={cn('min-w-0 truncate font-display text-lg neon-red tracking-wide', showMobileBack && 'hidden')} style={{
|
||||
textShadow: '0 0 8px #ff0040'
|
||||
}}>SKATESPOTS</span>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { genUserName } from '@/lib/genUserName';
|
|||
import type { MarketplaceItem } from '@/lib/types';
|
||||
import { PayWithSats } from '@/components/PayWithSats';
|
||||
import { MarkSoldDialog } from '@/components/MarkSoldDialog';
|
||||
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
||||
import { Handshake, CheckCircle, X, Loader2, ArrowLeftRight, Undo2, Zap } from 'lucide-react';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
|
|
@ -183,6 +184,8 @@ export function ListingOffersPanel({ item, isOwner }: { item: MarketplaceItem; i
|
|||
<Handshake className="h-4 w-4" /> {isOwner ? t('marketUi.offers.titleOwner', { count: mine.length }) : t('marketUi.offers.titleBuyer')}
|
||||
</h3>
|
||||
|
||||
{isOwner && <PaymentReadinessNote />}
|
||||
|
||||
{mine.map((thread) => {
|
||||
const latest = thread.latest;
|
||||
const other = latest.author === user.pubkey
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export function PaymentReadinessNote() {
|
|||
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-yellow-400" />
|
||||
<span>
|
||||
{t('marketUi.paymentReadiness.notReadyBefore')}{' '}
|
||||
<Link to="/settings" className="text-yellow-400 underline hover:text-yellow-300">
|
||||
<Link to="/settings?section=profile" className="text-yellow-400 underline hover:text-yellow-300">
|
||||
{t('marketUi.paymentReadiness.nostrProfile')}
|
||||
</Link>{' '}
|
||||
{t('marketUi.paymentReadiness.notReadyAfter')}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { MapPin, Store, Bookmark, MoreVertical, Flag, ShieldCheck } from 'lucide-react';
|
||||
import { MapPin, Store, Bookmark, MoreVertical, Flag, ShieldCheck, Trash2 } from 'lucide-react';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useVenuesWithPasses } from '@/hooks/useRiderCard';
|
||||
|
|
@ -23,6 +23,7 @@ import { OfficialBadge } from '@/components/OfficialBadge';
|
|||
import { AddToCollectionDialog } from '@/components/AddToCollectionDialog';
|
||||
import { ReportDialog } from '@/components/ReportDialog';
|
||||
import { MuteMenuItem } from '@/components/MuteMenuItem';
|
||||
import { DeleteEntityDialog } from '@/components/DeleteEntityDialog';
|
||||
import { SpotStatusBadge } from '@/components/SpotStatusBadge';
|
||||
import type { SkateSpot } from '@/lib/types';
|
||||
import { ACCESS_TYPE_INFO, SHELTER_INFO, SKATE_SHOP_KIND } from '@/lib/types';
|
||||
|
|
@ -30,6 +31,7 @@ import { spotTypeLabel } from '@/lib/spotTypeLabel';
|
|||
import { ACCESS_TYPE_KEYS, SHELTER_SHORT_KEYS } from '@/lib/labelKeys';
|
||||
import type { AggregatedSpotStatus } from '@/hooks/useSpotStatus';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
import { isAdminPubkey } from '@/lib/admins';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface SpotCardProps {
|
||||
|
|
@ -48,10 +50,13 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
|
|||
const displayName = metadata?.name ?? genUserName(spot.pubkey);
|
||||
const [showCollectionDialog, setShowCollectionDialog] = useState(false);
|
||||
const [showReportDialog, setShowReportDialog] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
// Determine if this is a shop or a spot
|
||||
const isShop = spot.types.includes('shop');
|
||||
const isOwner = user?.pubkey === spot.pubkey;
|
||||
const isAdmin = isAdminPubkey(user?.pubkey);
|
||||
const canDelete = !!user && (isOwner || isAdmin);
|
||||
const route = isShop ? `/shop/${spot.pubkey}/${spot.identifier}` : `/spot/${spot.pubkey}/${spot.identifier}`;
|
||||
|
||||
// Shops that actually issue access passes get a badge (one shared, cached query).
|
||||
|
|
@ -245,7 +250,19 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
|
|||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuContent align="end" className="w-44">
|
||||
{canDelete && (
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setShowDeleteDialog(true);
|
||||
}}
|
||||
className="text-red-500 focus:text-red-500"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{isOwner ? t('common.delete') : t('spotUi.removeAdmin')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!isOwner && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
|
|
@ -275,6 +292,17 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Owner/admin delete confirmation */}
|
||||
{canDelete && (
|
||||
<DeleteEntityDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
event={spot.event}
|
||||
entityLabel={isShop ? t('mapUi.skateShop') : t('spotUi.spot')}
|
||||
entityName={spot.title}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Report Dialog */}
|
||||
<ReportDialog
|
||||
open={showReportDialog}
|
||||
|
|
|
|||
|
|
@ -814,6 +814,11 @@ export function CordnNativePanel({
|
|||
</div>
|
||||
</div>
|
||||
<p className="pt-1 text-xs text-gray-500">{copy.subtitle}</p>
|
||||
{native.group && native.lastCheckedAt && (
|
||||
<p className="pt-0.5 text-[10px] text-gray-600">
|
||||
{t('cordnUi.lastChecked')} {formatDistanceToNow(new Date(native.lastCheckedAt), { addSuffix: true })}
|
||||
</p>
|
||||
)}
|
||||
{!isCommunity && (
|
||||
<details className="group mt-1 rounded-md border border-white/10 bg-black/20 px-2.5 py-1.5">
|
||||
<summary className="flex cursor-pointer list-none items-center gap-1 text-[11px] text-gray-400">
|
||||
|
|
@ -870,14 +875,29 @@ export function CordnNativePanel({
|
|||
</p>
|
||||
|
||||
<ol className="mx-auto max-w-sm space-y-2 text-left">
|
||||
{steps.map((step, index) => (
|
||||
<li key={step} className="flex items-start gap-2 text-xs text-gray-400">
|
||||
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-teal-500/15 text-[10px] font-bold text-teal-300">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span>{step}</span>
|
||||
</li>
|
||||
))}
|
||||
{steps.map((step, index) => {
|
||||
// Members see the setup move visibly: device key prepared →
|
||||
// request sent → waiting for an admin. Founders start at step 1.
|
||||
const stepDone = !canCreate && index === 0 && (awaiting || native.keyPackageReady);
|
||||
const stepWaiting = !canCreate && index === 1 && awaiting;
|
||||
return (
|
||||
<li
|
||||
key={step}
|
||||
className={cn(
|
||||
'flex items-start gap-2 text-xs',
|
||||
stepDone || stepWaiting ? 'text-teal-200' : 'text-gray-400',
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
'mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] font-bold',
|
||||
stepDone ? 'bg-teal-500/20 text-teal-300' : 'bg-teal-500/15 text-teal-300',
|
||||
)}>
|
||||
{stepDone ? <Check className="h-2.5 w-2.5" /> : stepWaiting ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : index + 1}
|
||||
</span>
|
||||
<span>{step}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
|
||||
<p className="mx-auto max-w-sm text-[11px] text-gray-500">
|
||||
|
|
|
|||
|
|
@ -47,6 +47,41 @@ function validateImage(file: File): void {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize/compress camera photos before upload. Most phone photos are far larger
|
||||
* than the app ever displays; 1600px WebP cuts upload time and Blossom storage
|
||||
* without touching GIFs or formats the WebView cannot decode.
|
||||
*/
|
||||
async function prepareImage(file: File): Promise<File> {
|
||||
const type = (file.type || '').toLowerCase();
|
||||
if (type === 'image/gif' || type === 'image/heic' || type === 'image/heif') return file;
|
||||
if (typeof createImageBitmap !== 'function' || typeof document === 'undefined') return file;
|
||||
try {
|
||||
const bitmap = await createImageBitmap(file);
|
||||
const scale = Math.min(1, 1600 / Math.max(bitmap.width, bitmap.height));
|
||||
// Already small enough: keep the original bytes/format.
|
||||
if (scale === 1 && file.size <= 1_200_000) {
|
||||
bitmap.close();
|
||||
return file;
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
||||
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) { bitmap.close(); return file; }
|
||||
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||
bitmap.close();
|
||||
const blob = await new Promise<Blob | null>((resolve) => {
|
||||
canvas.toBlob(resolve, 'image/webp', 0.84);
|
||||
});
|
||||
if (!blob || blob.size >= file.size) return file;
|
||||
const base = file.name.replace(/\.[^.]+$/, '') || 'image';
|
||||
return new File([blob], `${base}.webp`, { type: 'image/webp', lastModified: file.lastModified });
|
||||
} catch {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
function hostOf(url: string): string {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
|
|
@ -65,6 +100,7 @@ export function useUploadFile() {
|
|||
}
|
||||
|
||||
validateImage(file);
|
||||
const prepared = await prepareImage(file);
|
||||
|
||||
const uploader = new BlossomUploader({
|
||||
servers: UPLOAD_SERVERS,
|
||||
|
|
@ -73,7 +109,7 @@ export function useUploadFile() {
|
|||
|
||||
let tags;
|
||||
try {
|
||||
tags = await uploader.upload(file);
|
||||
tags = await uploader.upload(prepared);
|
||||
} catch (error) {
|
||||
throw new Error(uploadErrorMessage(error));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { useListingRestrictions, recordListingCreation } from '@/hooks/useListin
|
|||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { Nip05Required } from '@/components/Nip05Required';
|
||||
import { SpamFeePayment } from '@/components/SpamFeePayment';
|
||||
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
||||
import { MARKETPLACE_ITEM_KIND, GEAR_CATEGORIES, GEAR_CONDITIONS, PRICE_TYPES, SIZE_OPTIONS, TRUCK_HEIGHTS, WHEEL_HARDNESS, EXPIRATION_OPTIONS, CURRENCY_OPTIONS, CATEGORY_REQUIRED_FIELDS } from '@/lib/types';
|
||||
import { GEAR_CATEGORY_KEYS, GEAR_CONDITION_KEYS, GEAR_CONDITION_DESC_KEYS, PRICE_TYPE_KEYS, PRICE_TYPE_DESC_KEYS } from '@/lib/labelKeys';
|
||||
import { ShoppingBag, Upload, X, Loader2, ArrowLeft, MapPin, AlertTriangle, CheckCircle, Search, Clock, Shield, Zap } from 'lucide-react';
|
||||
|
|
@ -857,6 +858,8 @@ export default function CreateListingPage() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{listingType === 'selling' && <PaymentReadinessNote />}
|
||||
|
||||
{priceType === 'fixed' && (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 space-y-2">
|
||||
|
|
|
|||
Loading…
Reference in a new issue