Ticket waves: manage them, buy from them, stop lying about totals #176
7 changed files with 1366 additions and 1292 deletions
fix(events): stop showing a ticket total nothing publishes
The card and detail page rendered "{available} of {total} tickets left",
where total was derived as `available + sold`. That was already wrong
after any organizer edit re-baselined remaining (#143), and waves make
it meaningless: since events ext v1.6.8 `tickets_available` is the
ACTIVE WAVE's stock while `tickets_sold` counts every wave, so the two
no longer share a denominator. An event with 5 left in the open wave and
7 sold across earlier ones displayed "5 of 12".
Nothing publishes a capacity — no tag, no field — so nothing can honestly
show one. Both sites now use the existing `ticketsAvailable` string,
which says only what the data knows.
`EventTicketInfo.total` and the `ticketsRemainingOfTotal` string are
removed rather than left unused, so neither can be picked up again.
Closes #143. Whether to publish a real capacity remains open on
aiolabs/events#34, which narrowed it to either dropping the total (this)
or storing capacity and publishing an explicit `tickets_total` tag.
commit
072ca8817b
|
|
@ -1,331 +1,341 @@
|
|||
import type { LocaleMessages } from '../types'
|
||||
import type { LocaleMessages } from "../types";
|
||||
|
||||
const messages: LocaleMessages = {
|
||||
nav: {
|
||||
home: 'Home',
|
||||
title: 'Virtual Realm',
|
||||
directory: 'Directory',
|
||||
faq: 'FAQ',
|
||||
events: 'Events',
|
||||
market: 'Market',
|
||||
chat: 'Chat',
|
||||
login: 'Login',
|
||||
logout: 'Logout'
|
||||
home: "Home",
|
||||
title: "Virtual Realm",
|
||||
directory: "Directory",
|
||||
faq: "FAQ",
|
||||
events: "Events",
|
||||
market: "Market",
|
||||
chat: "Chat",
|
||||
login: "Login",
|
||||
logout: "Logout",
|
||||
},
|
||||
common: {
|
||||
loading: 'Loading...',
|
||||
error: 'An error occurred',
|
||||
success: 'Operation successful',
|
||||
loading: "Loading...",
|
||||
error: "An error occurred",
|
||||
success: "Operation successful",
|
||||
nav: {
|
||||
profile: 'Profile',
|
||||
preferences: 'Preferences',
|
||||
profileDescription: 'Your Nostr identity and display name.',
|
||||
profileLoggedOutDescription: 'Sign in or change your preferences.',
|
||||
login: 'Log in',
|
||||
menu: 'Menu',
|
||||
backToHub: 'Back to hub',
|
||||
hub: 'Hub',
|
||||
theme: 'Theme',
|
||||
themeLight: 'Light',
|
||||
themeDark: 'Dark',
|
||||
themeSystem: 'System',
|
||||
colorScheme: 'Color scheme',
|
||||
palette_catppuccin: 'Catppuccin',
|
||||
palette_countrysidecastle: 'Countryside Castle',
|
||||
palette_darkmatter: 'Dark Matter',
|
||||
palette_emeraldforest: 'Emerald Forest',
|
||||
palette_lightgreen: 'Light Green',
|
||||
palette_neobrut: 'Neo Brutalist',
|
||||
palette_starrynight: 'Starry Night',
|
||||
language: 'Language',
|
||||
currency: 'Currency',
|
||||
currencyComingSoon: 'Currency picker — coming soon',
|
||||
currencyComingSoonDescription: 'A preferred-currency setting (sats/USD/EUR) is on the roadmap.'
|
||||
}
|
||||
profile: "Profile",
|
||||
preferences: "Preferences",
|
||||
profileDescription: "Your Nostr identity and display name.",
|
||||
profileLoggedOutDescription: "Sign in or change your preferences.",
|
||||
login: "Log in",
|
||||
menu: "Menu",
|
||||
backToHub: "Back to hub",
|
||||
hub: "Hub",
|
||||
theme: "Theme",
|
||||
themeLight: "Light",
|
||||
themeDark: "Dark",
|
||||
themeSystem: "System",
|
||||
colorScheme: "Color scheme",
|
||||
palette_catppuccin: "Catppuccin",
|
||||
palette_countrysidecastle: "Countryside Castle",
|
||||
palette_darkmatter: "Dark Matter",
|
||||
palette_emeraldforest: "Emerald Forest",
|
||||
palette_lightgreen: "Light Green",
|
||||
palette_neobrut: "Neo Brutalist",
|
||||
palette_starrynight: "Starry Night",
|
||||
language: "Language",
|
||||
currency: "Currency",
|
||||
currencyComingSoon: "Currency picker — coming soon",
|
||||
currencyComingSoonDescription:
|
||||
"A preferred-currency setting (sats/USD/EUR) is on the roadmap.",
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
notFound: 'Page not found',
|
||||
serverError: 'Server error occurred',
|
||||
networkError: 'Network connection error'
|
||||
notFound: "Page not found",
|
||||
serverError: "Server error occurred",
|
||||
networkError: "Network connection error",
|
||||
},
|
||||
locales: {
|
||||
en: 'English',
|
||||
es: 'Spanish',
|
||||
fr: 'French',
|
||||
de: 'German',
|
||||
zh: 'Chinese'
|
||||
en: "English",
|
||||
es: "Spanish",
|
||||
fr: "French",
|
||||
de: "German",
|
||||
zh: "Chinese",
|
||||
},
|
||||
events: {
|
||||
title: 'Events',
|
||||
createNew: 'Create Event',
|
||||
title: "Events",
|
||||
createNew: "Create Event",
|
||||
create: {
|
||||
promo: {
|
||||
title: 'Promo codes',
|
||||
hint: 'Percentage discounts buyers can enter at checkout. Codes are never shown publicly.',
|
||||
empty: 'No promo codes yet.',
|
||||
code: 'Code',
|
||||
codePlaceholder: 'EARLYBIRD',
|
||||
discount: 'Discount %',
|
||||
maxUses: 'Max uses',
|
||||
active: 'Active',
|
||||
used: 'Used {count}',
|
||||
add: 'Add code',
|
||||
removeLabel: 'Remove code {code}',
|
||||
title: "Promo codes",
|
||||
hint: "Percentage discounts buyers can enter at checkout. Codes are never shown publicly.",
|
||||
empty: "No promo codes yet.",
|
||||
code: "Code",
|
||||
codePlaceholder: "EARLYBIRD",
|
||||
discount: "Discount %",
|
||||
maxUses: "Max uses",
|
||||
active: "Active",
|
||||
used: "Used {count}",
|
||||
add: "Add code",
|
||||
removeLabel: "Remove code {code}",
|
||||
errors: {
|
||||
required: 'Enter a code',
|
||||
duplicate: 'Duplicate code',
|
||||
percentRange: 'Between 1 and 100',
|
||||
maxUsesMin: 'Must be 1 or more',
|
||||
required: "Enter a code",
|
||||
duplicate: "Duplicate code",
|
||||
percentRange: "Between 1 and 100",
|
||||
maxUsesMin: "Must be 1 or more",
|
||||
},
|
||||
},
|
||||
},
|
||||
noEvents: 'No events found',
|
||||
noEvents: "No events found",
|
||||
filters: {
|
||||
all: 'All',
|
||||
today: 'Today',
|
||||
tomorrow: 'Tomorrow',
|
||||
thisWeek: 'This Week',
|
||||
thisMonth: 'This Month',
|
||||
myTickets: 'My tickets',
|
||||
hosting: 'Hosting',
|
||||
pastEvents: 'Past events',
|
||||
past: 'Past',
|
||||
filters: 'Filters',
|
||||
clearAll: 'Clear all',
|
||||
filteringBy: 'Filtering by:',
|
||||
removeCategory: 'Remove {category} filter',
|
||||
all: "All",
|
||||
today: "Today",
|
||||
tomorrow: "Tomorrow",
|
||||
thisWeek: "This Week",
|
||||
thisMonth: "This Month",
|
||||
myTickets: "My tickets",
|
||||
hosting: "Hosting",
|
||||
pastEvents: "Past events",
|
||||
past: "Past",
|
||||
filters: "Filters",
|
||||
clearAll: "Clear all",
|
||||
filteringBy: "Filtering by:",
|
||||
removeCategory: "Remove {category} filter",
|
||||
},
|
||||
categories: {
|
||||
concert: 'Concert',
|
||||
workshop: 'Workshop',
|
||||
market: 'Market',
|
||||
festival: 'Festival',
|
||||
exhibition: 'Exhibition',
|
||||
sport: 'Sport',
|
||||
theater: 'Theater',
|
||||
cinema: 'Cinema',
|
||||
party: 'Party',
|
||||
talk: 'Talk',
|
||||
conference: 'Conference',
|
||||
meetup: 'Meetup',
|
||||
food: 'Food',
|
||||
outdoor: 'Outdoor',
|
||||
kids: 'Kids',
|
||||
wellness: 'Wellness',
|
||||
technology: 'Technology',
|
||||
art: 'Art',
|
||||
music: 'Music',
|
||||
dance: 'Dance',
|
||||
literature: 'Literature',
|
||||
comedy: 'Comedy',
|
||||
charity: 'Charity',
|
||||
tradition: 'Tradition',
|
||||
other: 'Other',
|
||||
concert: "Concert",
|
||||
workshop: "Workshop",
|
||||
market: "Market",
|
||||
festival: "Festival",
|
||||
exhibition: "Exhibition",
|
||||
sport: "Sport",
|
||||
theater: "Theater",
|
||||
cinema: "Cinema",
|
||||
party: "Party",
|
||||
talk: "Talk",
|
||||
conference: "Conference",
|
||||
meetup: "Meetup",
|
||||
food: "Food",
|
||||
outdoor: "Outdoor",
|
||||
kids: "Kids",
|
||||
wellness: "Wellness",
|
||||
technology: "Technology",
|
||||
art: "Art",
|
||||
music: "Music",
|
||||
dance: "Dance",
|
||||
literature: "Literature",
|
||||
comedy: "Comedy",
|
||||
charity: "Charity",
|
||||
tradition: "Tradition",
|
||||
other: "Other",
|
||||
},
|
||||
detail: {
|
||||
getTicket: 'Get Ticket',
|
||||
contactOrganizer: 'Contact Organizer',
|
||||
organizer: 'Organizer',
|
||||
location: 'Location',
|
||||
when: 'When',
|
||||
tickets: 'Tickets',
|
||||
ticketsAvailable: '{count} tickets available',
|
||||
ticketsRemainingOfTotal: '{count} of {total} tickets left',
|
||||
ticketsOwned: 'You have {count} ticket | You have {count} tickets',
|
||||
unlimitedTickets: 'Unlimited tickets',
|
||||
buyTicket: 'Buy ticket',
|
||||
buyAnotherTicket: 'Buy another ticket',
|
||||
viewMyTickets: 'View in My Tickets',
|
||||
soldOut: 'Sold Out',
|
||||
pastEvent: 'This event has already happened',
|
||||
loginToBuyTickets: 'Log in to buy tickets',
|
||||
logIn: 'Log in',
|
||||
free: 'Free',
|
||||
getTicket: "Get Ticket",
|
||||
contactOrganizer: "Contact Organizer",
|
||||
organizer: "Organizer",
|
||||
location: "Location",
|
||||
when: "When",
|
||||
tickets: "Tickets",
|
||||
ticketsAvailable: "{count} tickets available",
|
||||
ticketsOwned: "You have {count} ticket | You have {count} tickets",
|
||||
unlimitedTickets: "Unlimited tickets",
|
||||
buyTicket: "Buy ticket",
|
||||
buyAnotherTicket: "Buy another ticket",
|
||||
viewMyTickets: "View in My Tickets",
|
||||
soldOut: "Sold Out",
|
||||
pastEvent: "This event has already happened",
|
||||
loginToBuyTickets: "Log in to buy tickets",
|
||||
logIn: "Log in",
|
||||
free: "Free",
|
||||
},
|
||||
tickets: {
|
||||
myTickets: 'My Tickets',
|
||||
scanTicket: 'Scan Ticket',
|
||||
noTickets: 'No tickets yet',
|
||||
paid: 'Paid',
|
||||
pending: 'Pending',
|
||||
registered: 'Registered',
|
||||
myTickets: "My Tickets",
|
||||
scanTicket: "Scan Ticket",
|
||||
noTickets: "No tickets yet",
|
||||
paid: "Paid",
|
||||
pending: "Pending",
|
||||
registered: "Registered",
|
||||
},
|
||||
purchase: {
|
||||
card: 'Card',
|
||||
continueTo: 'Continue to {provider} checkout',
|
||||
fiatRedirectHint: "You'll be taken to the provider's secure checkout and brought back here once paid.",
|
||||
noCheckoutUrl: 'Fiat provider did not return a checkout URL.',
|
||||
fiatFailed: 'Card checkout failed',
|
||||
loginRequiredTitle: 'Login required',
|
||||
loginRequiredFree: 'Free tickets need an account so seats cannot be claimed anonymously. Log in to get yours.',
|
||||
card: "Card",
|
||||
continueTo: "Continue to {provider} checkout",
|
||||
fiatRedirectHint:
|
||||
"You'll be taken to the provider's secure checkout and brought back here once paid.",
|
||||
noCheckoutUrl: "Fiat provider did not return a checkout URL.",
|
||||
fiatFailed: "Card checkout failed",
|
||||
loginRequiredTitle: "Login required",
|
||||
loginRequiredFree:
|
||||
"Free tickets need an account so seats cannot be claimed anonymously. Log in to get yours.",
|
||||
promo: {
|
||||
label: 'Promo code',
|
||||
placeholder: 'Enter code',
|
||||
apply: 'Apply',
|
||||
remove: 'Remove',
|
||||
checking: 'Checking…',
|
||||
applied: '{code} −{percent}%',
|
||||
discountLine: 'Discount ({code})',
|
||||
invalid: 'This code is not valid for this purchase.',
|
||||
unverified: 'Could not verify the code — it will be applied at checkout if valid.',
|
||||
loginRequiredDiscount: 'This code makes your ticket free. Free tickets need an account — log in to claim it.',
|
||||
label: "Promo code",
|
||||
placeholder: "Enter code",
|
||||
apply: "Apply",
|
||||
remove: "Remove",
|
||||
checking: "Checking…",
|
||||
applied: "{code} −{percent}%",
|
||||
discountLine: "Discount ({code})",
|
||||
invalid: "This code is not valid for this purchase.",
|
||||
unverified:
|
||||
"Could not verify the code — it will be applied at checkout if valid.",
|
||||
loginRequiredDiscount:
|
||||
"This code makes your ticket free. Free tickets need an account — log in to claim it.",
|
||||
},
|
||||
contact: {
|
||||
title: 'Your details',
|
||||
name: 'Name',
|
||||
namePlaceholder: 'Name on the ticket',
|
||||
nameRequired: 'Name is required',
|
||||
email: 'Email',
|
||||
emailHint: 'Your ticket will be sent to this address.',
|
||||
spamHint: 'If it has not arrived a few minutes after paying, check your spam folder.',
|
||||
emailInvalid: 'Enter a valid email address',
|
||||
confirmEmail: 'Confirm email',
|
||||
emailMismatch: 'Email addresses do not match',
|
||||
title: "Your details",
|
||||
name: "Name",
|
||||
namePlaceholder: "Name on the ticket",
|
||||
nameRequired: "Name is required",
|
||||
email: "Email",
|
||||
emailHint: "Your ticket will be sent to this address.",
|
||||
spamHint:
|
||||
"If it has not arrived a few minutes after paying, check your spam folder.",
|
||||
emailInvalid: "Enter a valid email address",
|
||||
confirmEmail: "Confirm email",
|
||||
emailMismatch: "Email addresses do not match",
|
||||
},
|
||||
},
|
||||
checkout: {
|
||||
title: 'Confirming your payment…',
|
||||
paidTitle: 'Payment received',
|
||||
processing: 'Waiting for the payment to settle. This usually takes a few seconds.',
|
||||
stillProcessing: 'Still processing. Your ticket will be emailed to {email} as soon as the payment settles.',
|
||||
stillProcessingNoEmail: 'Still processing. Reload this page in a moment to check again.',
|
||||
emailedTo: 'We emailed your ticket to {email}.',
|
||||
cancelled: 'Payment cancelled — no seats were reserved.',
|
||||
viewTicket: 'Open ticket page',
|
||||
viewMyTickets: 'View My Tickets',
|
||||
retry: 'Check again',
|
||||
ticketN: 'Ticket {n} of {total}',
|
||||
close: 'Close',
|
||||
title: "Confirming your payment…",
|
||||
paidTitle: "Payment received",
|
||||
processing:
|
||||
"Waiting for the payment to settle. This usually takes a few seconds.",
|
||||
stillProcessing:
|
||||
"Still processing. Your ticket will be emailed to {email} as soon as the payment settles.",
|
||||
stillProcessingNoEmail:
|
||||
"Still processing. Reload this page in a moment to check again.",
|
||||
emailedTo: "We emailed your ticket to {email}.",
|
||||
cancelled: "Payment cancelled — no seats were reserved.",
|
||||
viewTicket: "Open ticket page",
|
||||
viewMyTickets: "View My Tickets",
|
||||
retry: "Check again",
|
||||
ticketN: "Ticket {n} of {total}",
|
||||
close: "Close",
|
||||
},
|
||||
ticket: {
|
||||
title: 'Your ticket',
|
||||
loading: 'Loading ticket…',
|
||||
notFound: 'Ticket not found',
|
||||
event: 'Event',
|
||||
holder: 'Name',
|
||||
paid: 'Paid',
|
||||
pending: 'Payment pending',
|
||||
registered: 'Checked in',
|
||||
showAtDoor: 'Show this QR at the door.',
|
||||
backToEvent: 'Back to event',
|
||||
title: "Your ticket",
|
||||
loading: "Loading ticket…",
|
||||
notFound: "Ticket not found",
|
||||
event: "Event",
|
||||
holder: "Name",
|
||||
paid: "Paid",
|
||||
pending: "Payment pending",
|
||||
registered: "Checked in",
|
||||
showAtDoor: "Show this QR at the door.",
|
||||
backToEvent: "Back to event",
|
||||
},
|
||||
nav: {
|
||||
feed: 'Home',
|
||||
calendar: 'Calendar',
|
||||
map: 'Map',
|
||||
favorites: 'Favorites',
|
||||
settings: 'Settings',
|
||||
feed: "Home",
|
||||
calendar: "Calendar",
|
||||
map: "Map",
|
||||
favorites: "Favorites",
|
||||
settings: "Settings",
|
||||
},
|
||||
search: {
|
||||
placeholder: 'Search events...',
|
||||
noResults: 'No events found',
|
||||
placeholder: "Search events...",
|
||||
noResults: "No events found",
|
||||
},
|
||||
favorites: {
|
||||
title: 'Favorites',
|
||||
loginPrompt: 'Log in to save your favorite events',
|
||||
empty: 'No favorites yet',
|
||||
emptyHint: 'Tap the heart icon on any event to save it here',
|
||||
logIn: 'Log in',
|
||||
title: "Favorites",
|
||||
loginPrompt: "Log in to save your favorite events",
|
||||
empty: "No favorites yet",
|
||||
emptyHint: "Tap the heart icon on any event to save it here",
|
||||
logIn: "Log in",
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
account: 'Account',
|
||||
loginPrompt: 'Log in to bookmark events, RSVP, and purchase tickets.',
|
||||
logIn: 'Log in',
|
||||
logOut: 'Log out',
|
||||
appearance: 'Appearance',
|
||||
theme: 'Theme',
|
||||
language: 'Language',
|
||||
title: "Settings",
|
||||
account: "Account",
|
||||
loginPrompt: "Log in to bookmark events, RSVP, and purchase tickets.",
|
||||
logIn: "Log in",
|
||||
logOut: "Log out",
|
||||
appearance: "Appearance",
|
||||
theme: "Theme",
|
||||
language: "Language",
|
||||
},
|
||||
},
|
||||
libra: {
|
||||
nav: {
|
||||
record: 'Record',
|
||||
transactions: 'Transactions',
|
||||
balance: 'Balance',
|
||||
wallet: 'Wallet',
|
||||
settings: 'Settings',
|
||||
record: "Record",
|
||||
transactions: "Transactions",
|
||||
balance: "Balance",
|
||||
wallet: "Wallet",
|
||||
settings: "Settings",
|
||||
},
|
||||
record: {
|
||||
title: 'Record',
|
||||
addExpense: 'Add Expense',
|
||||
addExpenseDescription: 'Submit an expense for approval',
|
||||
addIncome: 'Add Income',
|
||||
addIncomeDescription: 'Record income or revenue received',
|
||||
comingSoon: 'Coming Soon',
|
||||
drafts: 'Drafts',
|
||||
noDrafts: 'No saved drafts',
|
||||
draftAge: 'Saved {time}',
|
||||
saveDraft: 'Save Draft',
|
||||
deleteDraft: 'Delete Draft',
|
||||
title: "Record",
|
||||
addExpense: "Add Expense",
|
||||
addExpenseDescription: "Submit an expense for approval",
|
||||
addIncome: "Add Income",
|
||||
addIncomeDescription: "Record income or revenue received",
|
||||
comingSoon: "Coming Soon",
|
||||
drafts: "Drafts",
|
||||
noDrafts: "No saved drafts",
|
||||
draftAge: "Saved {time}",
|
||||
saveDraft: "Save Draft",
|
||||
deleteDraft: "Delete Draft",
|
||||
},
|
||||
balance: {
|
||||
title: 'Balance',
|
||||
netBalance: 'Net Balance',
|
||||
pending: 'Pending',
|
||||
pendingExpenses: '{count} pending expense | {count} pending expenses',
|
||||
pendingAmount: '{amount} pending approval',
|
||||
noBalance: 'No balance data available',
|
||||
owedToYou: 'Owed to you',
|
||||
youOwe: 'You owe',
|
||||
title: "Balance",
|
||||
netBalance: "Net Balance",
|
||||
pending: "Pending",
|
||||
pendingExpenses: "{count} pending expense | {count} pending expenses",
|
||||
pendingAmount: "{amount} pending approval",
|
||||
noBalance: "No balance data available",
|
||||
owedToYou: "Owed to you",
|
||||
youOwe: "You owe",
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
account: 'Account',
|
||||
loginPrompt: 'Log in to record expenses and view your balance.',
|
||||
logIn: 'Log in',
|
||||
logOut: 'Log out',
|
||||
appearance: 'Appearance',
|
||||
theme: 'Theme',
|
||||
language: 'Language',
|
||||
title: "Settings",
|
||||
account: "Account",
|
||||
loginPrompt: "Log in to record expenses and view your balance.",
|
||||
logIn: "Log in",
|
||||
logOut: "Log out",
|
||||
appearance: "Appearance",
|
||||
theme: "Theme",
|
||||
language: "Language",
|
||||
},
|
||||
income: {
|
||||
title: 'Add Income',
|
||||
description: 'Submit income for the organization',
|
||||
selectAccount: 'Select the revenue account.',
|
||||
title: "Add Income",
|
||||
description: "Submit income for the organization",
|
||||
selectAccount: "Select the revenue account.",
|
||||
otherAccountHint: 'Use the "Other" account if you\'re not sure.',
|
||||
submitIncome: 'Submit Income',
|
||||
notAvailable: 'Income submission is not yet available. This feature is coming soon.',
|
||||
submitIncome: "Submit Income",
|
||||
notAvailable:
|
||||
"Income submission is not yet available. This feature is coming soon.",
|
||||
},
|
||||
},
|
||||
market: {
|
||||
auth: {
|
||||
loginPrompt: 'Log in to place your order',
|
||||
logIn: 'Log in',
|
||||
logInToCheckout: 'Log in to checkout',
|
||||
nostrKeyRequired: 'A Nostr identity is required',
|
||||
nostrKeyDescription: 'Configure your Nostr public key in Profile settings to place orders.',
|
||||
loginPrompt: "Log in to place your order",
|
||||
logIn: "Log in",
|
||||
logInToCheckout: "Log in to checkout",
|
||||
nostrKeyRequired: "A Nostr identity is required",
|
||||
nostrKeyDescription:
|
||||
"Configure your Nostr public key in Profile settings to place orders.",
|
||||
},
|
||||
},
|
||||
dateTimeFormats: {
|
||||
short: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
},
|
||||
long: {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
}
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
weekday: "long",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
},
|
||||
},
|
||||
numberFormats: {
|
||||
currency: {
|
||||
style: 'currency',
|
||||
currencyDisplay: 'symbol'
|
||||
style: "currency",
|
||||
currencyDisplay: "symbol",
|
||||
},
|
||||
decimal: {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: 2
|
||||
style: "decimal",
|
||||
minimumFractionDigits: 2,
|
||||
},
|
||||
percent: {
|
||||
style: 'percent',
|
||||
useGrouping: false
|
||||
}
|
||||
}
|
||||
}
|
||||
style: "percent",
|
||||
useGrouping: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default messages
|
||||
export default messages;
|
||||
|
|
|
|||
|
|
@ -1,331 +1,342 @@
|
|||
import type { LocaleMessages } from '../types'
|
||||
import type { LocaleMessages } from "../types";
|
||||
|
||||
const messages: LocaleMessages = {
|
||||
nav: {
|
||||
title: 'Reino Virtual',
|
||||
home: 'Inicio',
|
||||
directory: 'Directorio',
|
||||
faq: 'Preguntas Frecuentes',
|
||||
events: 'Eventos',
|
||||
market: 'Mercado',
|
||||
chat: 'Chat',
|
||||
login: 'Iniciar Sesión',
|
||||
logout: 'Cerrar Sesión'
|
||||
title: "Reino Virtual",
|
||||
home: "Inicio",
|
||||
directory: "Directorio",
|
||||
faq: "Preguntas Frecuentes",
|
||||
events: "Eventos",
|
||||
market: "Mercado",
|
||||
chat: "Chat",
|
||||
login: "Iniciar Sesión",
|
||||
logout: "Cerrar Sesión",
|
||||
},
|
||||
common: {
|
||||
loading: 'Cargando...',
|
||||
error: 'Ha ocurrido un error',
|
||||
success: 'Operación exitosa',
|
||||
loading: "Cargando...",
|
||||
error: "Ha ocurrido un error",
|
||||
success: "Operación exitosa",
|
||||
nav: {
|
||||
profile: 'Perfil',
|
||||
preferences: 'Preferencias',
|
||||
profileDescription: 'Tu identidad Nostr y nombre de visualización.',
|
||||
profileLoggedOutDescription: 'Inicia sesión o cambia tus preferencias.',
|
||||
login: 'Iniciar sesión',
|
||||
menu: 'Menú',
|
||||
backToHub: 'Volver al hub',
|
||||
hub: 'Hub',
|
||||
theme: 'Tema',
|
||||
themeLight: 'Claro',
|
||||
themeDark: 'Oscuro',
|
||||
themeSystem: 'Sistema',
|
||||
colorScheme: 'Paleta',
|
||||
palette_catppuccin: 'Catppuccin',
|
||||
palette_countrysidecastle: 'Castillo Campestre',
|
||||
palette_darkmatter: 'Dark Matter',
|
||||
palette_emeraldforest: 'Bosque Esmeralda',
|
||||
palette_lightgreen: 'Verde Claro',
|
||||
palette_neobrut: 'Neo Brutalista',
|
||||
palette_starrynight: 'Noche Estrellada',
|
||||
language: 'Idioma',
|
||||
currency: 'Moneda',
|
||||
currencyComingSoon: 'Selector de moneda — próximamente',
|
||||
currencyComingSoonDescription: 'Un ajuste de moneda preferida (sats/USD/EUR) está en la hoja de ruta.'
|
||||
}
|
||||
profile: "Perfil",
|
||||
preferences: "Preferencias",
|
||||
profileDescription: "Tu identidad Nostr y nombre de visualización.",
|
||||
profileLoggedOutDescription: "Inicia sesión o cambia tus preferencias.",
|
||||
login: "Iniciar sesión",
|
||||
menu: "Menú",
|
||||
backToHub: "Volver al hub",
|
||||
hub: "Hub",
|
||||
theme: "Tema",
|
||||
themeLight: "Claro",
|
||||
themeDark: "Oscuro",
|
||||
themeSystem: "Sistema",
|
||||
colorScheme: "Paleta",
|
||||
palette_catppuccin: "Catppuccin",
|
||||
palette_countrysidecastle: "Castillo Campestre",
|
||||
palette_darkmatter: "Dark Matter",
|
||||
palette_emeraldforest: "Bosque Esmeralda",
|
||||
palette_lightgreen: "Verde Claro",
|
||||
palette_neobrut: "Neo Brutalista",
|
||||
palette_starrynight: "Noche Estrellada",
|
||||
language: "Idioma",
|
||||
currency: "Moneda",
|
||||
currencyComingSoon: "Selector de moneda — próximamente",
|
||||
currencyComingSoonDescription:
|
||||
"Un ajuste de moneda preferida (sats/USD/EUR) está en la hoja de ruta.",
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
notFound: 'Página no encontrada',
|
||||
serverError: 'Error del servidor',
|
||||
networkError: 'Error de conexión de red'
|
||||
notFound: "Página no encontrada",
|
||||
serverError: "Error del servidor",
|
||||
networkError: "Error de conexión de red",
|
||||
},
|
||||
locales: {
|
||||
en: 'Inglés',
|
||||
es: 'Español',
|
||||
fr: 'Francés',
|
||||
de: 'Alemán',
|
||||
zh: 'Chino'
|
||||
en: "Inglés",
|
||||
es: "Español",
|
||||
fr: "Francés",
|
||||
de: "Alemán",
|
||||
zh: "Chino",
|
||||
},
|
||||
events: {
|
||||
title: 'Eventos',
|
||||
createNew: 'Crear evento',
|
||||
title: "Eventos",
|
||||
createNew: "Crear evento",
|
||||
create: {
|
||||
promo: {
|
||||
title: 'Códigos promocionales',
|
||||
hint: 'Descuentos porcentuales que los compradores pueden introducir al pagar. Los códigos nunca se muestran públicamente.',
|
||||
empty: 'Aún no hay códigos promocionales.',
|
||||
code: 'Código',
|
||||
codePlaceholder: 'EARLYBIRD',
|
||||
discount: 'Descuento %',
|
||||
maxUses: 'Usos máx.',
|
||||
active: 'Activo',
|
||||
used: 'Usado {count}',
|
||||
add: 'Añadir código',
|
||||
removeLabel: 'Eliminar el código {code}',
|
||||
title: "Códigos promocionales",
|
||||
hint: "Descuentos porcentuales que los compradores pueden introducir al pagar. Los códigos nunca se muestran públicamente.",
|
||||
empty: "Aún no hay códigos promocionales.",
|
||||
code: "Código",
|
||||
codePlaceholder: "EARLYBIRD",
|
||||
discount: "Descuento %",
|
||||
maxUses: "Usos máx.",
|
||||
active: "Activo",
|
||||
used: "Usado {count}",
|
||||
add: "Añadir código",
|
||||
removeLabel: "Eliminar el código {code}",
|
||||
errors: {
|
||||
required: 'Introduce un código',
|
||||
duplicate: 'Código duplicado',
|
||||
percentRange: 'Entre 1 y 100',
|
||||
maxUsesMin: 'Debe ser 1 o más',
|
||||
required: "Introduce un código",
|
||||
duplicate: "Código duplicado",
|
||||
percentRange: "Entre 1 y 100",
|
||||
maxUsesMin: "Debe ser 1 o más",
|
||||
},
|
||||
},
|
||||
},
|
||||
noEvents: 'No se encontraron eventos',
|
||||
noEvents: "No se encontraron eventos",
|
||||
filters: {
|
||||
all: 'Todas',
|
||||
today: 'Hoy',
|
||||
tomorrow: 'Mañana',
|
||||
thisWeek: 'Esta semana',
|
||||
thisMonth: 'Este mes',
|
||||
myTickets: 'Mis boletos',
|
||||
hosting: 'Organizo',
|
||||
pastEvents: 'Eventos pasados',
|
||||
past: 'Pasado',
|
||||
filters: 'Filtros',
|
||||
clearAll: 'Limpiar todo',
|
||||
filteringBy: 'Filtrando por:',
|
||||
removeCategory: 'Quitar el filtro {category}',
|
||||
all: "Todas",
|
||||
today: "Hoy",
|
||||
tomorrow: "Mañana",
|
||||
thisWeek: "Esta semana",
|
||||
thisMonth: "Este mes",
|
||||
myTickets: "Mis boletos",
|
||||
hosting: "Organizo",
|
||||
pastEvents: "Eventos pasados",
|
||||
past: "Pasado",
|
||||
filters: "Filtros",
|
||||
clearAll: "Limpiar todo",
|
||||
filteringBy: "Filtrando por:",
|
||||
removeCategory: "Quitar el filtro {category}",
|
||||
},
|
||||
categories: {
|
||||
concert: 'Concierto',
|
||||
workshop: 'Taller',
|
||||
market: 'Mercado',
|
||||
festival: 'Festival',
|
||||
exhibition: 'Exposición',
|
||||
sport: 'Deporte',
|
||||
theater: 'Teatro',
|
||||
cinema: 'Cine',
|
||||
party: 'Fiesta',
|
||||
talk: 'Charla',
|
||||
conference: 'Conferencia',
|
||||
meetup: 'Encuentro',
|
||||
food: 'Gastronomía',
|
||||
outdoor: 'Al aire libre',
|
||||
kids: 'Niños',
|
||||
wellness: 'Bienestar',
|
||||
technology: 'Tecnología',
|
||||
art: 'Arte',
|
||||
music: 'Música',
|
||||
dance: 'Danza',
|
||||
literature: 'Literatura',
|
||||
comedy: 'Comedia',
|
||||
charity: 'Solidario',
|
||||
tradition: 'Tradición',
|
||||
other: 'Otro',
|
||||
concert: "Concierto",
|
||||
workshop: "Taller",
|
||||
market: "Mercado",
|
||||
festival: "Festival",
|
||||
exhibition: "Exposición",
|
||||
sport: "Deporte",
|
||||
theater: "Teatro",
|
||||
cinema: "Cine",
|
||||
party: "Fiesta",
|
||||
talk: "Charla",
|
||||
conference: "Conferencia",
|
||||
meetup: "Encuentro",
|
||||
food: "Gastronomía",
|
||||
outdoor: "Al aire libre",
|
||||
kids: "Niños",
|
||||
wellness: "Bienestar",
|
||||
technology: "Tecnología",
|
||||
art: "Arte",
|
||||
music: "Música",
|
||||
dance: "Danza",
|
||||
literature: "Literatura",
|
||||
comedy: "Comedia",
|
||||
charity: "Solidario",
|
||||
tradition: "Tradición",
|
||||
other: "Otro",
|
||||
},
|
||||
detail: {
|
||||
getTicket: 'Obtener boleto',
|
||||
contactOrganizer: 'Contactar organizador',
|
||||
organizer: 'Organizador',
|
||||
location: 'Ubicación',
|
||||
when: 'Cuándo',
|
||||
tickets: 'Boletos',
|
||||
ticketsAvailable: '{count} boletos disponibles',
|
||||
ticketsRemainingOfTotal: '{count} de {total} boletos restantes',
|
||||
ticketsOwned: 'Tienes {count} boleto | Tienes {count} boletos',
|
||||
unlimitedTickets: 'Boletos ilimitados',
|
||||
buyTicket: 'Comprar boleto',
|
||||
buyAnotherTicket: 'Comprar otro boleto',
|
||||
viewMyTickets: 'Ver en Mis boletos',
|
||||
soldOut: 'Agotado',
|
||||
pastEvent: 'Este evento ya pasó',
|
||||
loginToBuyTickets: 'Inicia sesión para comprar boletos',
|
||||
logIn: 'Iniciar sesión',
|
||||
free: 'Gratis',
|
||||
getTicket: "Obtener boleto",
|
||||
contactOrganizer: "Contactar organizador",
|
||||
organizer: "Organizador",
|
||||
location: "Ubicación",
|
||||
when: "Cuándo",
|
||||
tickets: "Boletos",
|
||||
ticketsAvailable: "{count} boletos disponibles",
|
||||
ticketsOwned: "Tienes {count} boleto | Tienes {count} boletos",
|
||||
unlimitedTickets: "Boletos ilimitados",
|
||||
buyTicket: "Comprar boleto",
|
||||
buyAnotherTicket: "Comprar otro boleto",
|
||||
viewMyTickets: "Ver en Mis boletos",
|
||||
soldOut: "Agotado",
|
||||
pastEvent: "Este evento ya pasó",
|
||||
loginToBuyTickets: "Inicia sesión para comprar boletos",
|
||||
logIn: "Iniciar sesión",
|
||||
free: "Gratis",
|
||||
},
|
||||
tickets: {
|
||||
myTickets: 'Mis boletos',
|
||||
scanTicket: 'Escanear boleto',
|
||||
noTickets: 'Aún no tienes boletos',
|
||||
paid: 'Pagado',
|
||||
pending: 'Pendiente',
|
||||
registered: 'Registrado',
|
||||
myTickets: "Mis boletos",
|
||||
scanTicket: "Escanear boleto",
|
||||
noTickets: "Aún no tienes boletos",
|
||||
paid: "Pagado",
|
||||
pending: "Pendiente",
|
||||
registered: "Registrado",
|
||||
},
|
||||
purchase: {
|
||||
card: 'Tarjeta',
|
||||
continueTo: 'Continuar al pago con {provider}',
|
||||
fiatRedirectHint: 'Te llevaremos al pago seguro del proveedor y volverás aquí una vez pagado.',
|
||||
noCheckoutUrl: 'El proveedor no devolvió una URL de pago.',
|
||||
fiatFailed: 'El pago con tarjeta falló',
|
||||
loginRequiredTitle: 'Inicio de sesión requerido',
|
||||
loginRequiredFree: 'Los boletos gratuitos requieren una cuenta para evitar reservas anónimas. Inicia sesión para obtener el tuyo.',
|
||||
card: "Tarjeta",
|
||||
continueTo: "Continuar al pago con {provider}",
|
||||
fiatRedirectHint:
|
||||
"Te llevaremos al pago seguro del proveedor y volverás aquí una vez pagado.",
|
||||
noCheckoutUrl: "El proveedor no devolvió una URL de pago.",
|
||||
fiatFailed: "El pago con tarjeta falló",
|
||||
loginRequiredTitle: "Inicio de sesión requerido",
|
||||
loginRequiredFree:
|
||||
"Los boletos gratuitos requieren una cuenta para evitar reservas anónimas. Inicia sesión para obtener el tuyo.",
|
||||
promo: {
|
||||
label: 'Código promocional',
|
||||
placeholder: 'Introduce el código',
|
||||
apply: 'Aplicar',
|
||||
remove: 'Quitar',
|
||||
checking: 'Comprobando…',
|
||||
applied: '{code} −{percent}%',
|
||||
discountLine: 'Descuento ({code})',
|
||||
invalid: 'Este código no es válido para esta compra.',
|
||||
unverified: 'No se pudo verificar el código; se aplicará al pagar si es válido.',
|
||||
loginRequiredDiscount: 'Este código hace que tu boleto sea gratis. Los boletos gratuitos requieren una cuenta: inicia sesión para reclamarlo.',
|
||||
label: "Código promocional",
|
||||
placeholder: "Introduce el código",
|
||||
apply: "Aplicar",
|
||||
remove: "Quitar",
|
||||
checking: "Comprobando…",
|
||||
applied: "{code} −{percent}%",
|
||||
discountLine: "Descuento ({code})",
|
||||
invalid: "Este código no es válido para esta compra.",
|
||||
unverified:
|
||||
"No se pudo verificar el código; se aplicará al pagar si es válido.",
|
||||
loginRequiredDiscount:
|
||||
"Este código hace que tu boleto sea gratis. Los boletos gratuitos requieren una cuenta: inicia sesión para reclamarlo.",
|
||||
},
|
||||
contact: {
|
||||
title: 'Tus datos',
|
||||
name: 'Nombre',
|
||||
namePlaceholder: 'Nombre en el boleto',
|
||||
nameRequired: 'El nombre es obligatorio',
|
||||
email: 'Correo electrónico',
|
||||
emailHint: 'Tu boleto se enviará a esta dirección.',
|
||||
spamHint: 'Si no llega unos minutos después de pagar, revisa tu carpeta de spam.',
|
||||
emailInvalid: 'Introduce un correo electrónico válido',
|
||||
confirmEmail: 'Confirmar correo',
|
||||
emailMismatch: 'Los correos no coinciden',
|
||||
title: "Tus datos",
|
||||
name: "Nombre",
|
||||
namePlaceholder: "Nombre en el boleto",
|
||||
nameRequired: "El nombre es obligatorio",
|
||||
email: "Correo electrónico",
|
||||
emailHint: "Tu boleto se enviará a esta dirección.",
|
||||
spamHint:
|
||||
"Si no llega unos minutos después de pagar, revisa tu carpeta de spam.",
|
||||
emailInvalid: "Introduce un correo electrónico válido",
|
||||
confirmEmail: "Confirmar correo",
|
||||
emailMismatch: "Los correos no coinciden",
|
||||
},
|
||||
},
|
||||
checkout: {
|
||||
title: 'Confirmando tu pago…',
|
||||
paidTitle: 'Pago recibido',
|
||||
processing: 'Esperando a que se liquide el pago. Suele tardar unos segundos.',
|
||||
stillProcessing: 'Aún en proceso. Tu boleto se enviará a {email} en cuanto se liquide el pago.',
|
||||
stillProcessingNoEmail: 'Aún en proceso. Recarga esta página en un momento.',
|
||||
emailedTo: 'Enviamos tu boleto a {email}.',
|
||||
cancelled: 'Pago cancelado — no se reservó ningún asiento.',
|
||||
viewTicket: 'Abrir página del boleto',
|
||||
viewMyTickets: 'Ver mis boletos',
|
||||
retry: 'Comprobar de nuevo',
|
||||
ticketN: 'Boleto {n} de {total}',
|
||||
close: 'Cerrar',
|
||||
title: "Confirmando tu pago…",
|
||||
paidTitle: "Pago recibido",
|
||||
processing:
|
||||
"Esperando a que se liquide el pago. Suele tardar unos segundos.",
|
||||
stillProcessing:
|
||||
"Aún en proceso. Tu boleto se enviará a {email} en cuanto se liquide el pago.",
|
||||
stillProcessingNoEmail:
|
||||
"Aún en proceso. Recarga esta página en un momento.",
|
||||
emailedTo: "Enviamos tu boleto a {email}.",
|
||||
cancelled: "Pago cancelado — no se reservó ningún asiento.",
|
||||
viewTicket: "Abrir página del boleto",
|
||||
viewMyTickets: "Ver mis boletos",
|
||||
retry: "Comprobar de nuevo",
|
||||
ticketN: "Boleto {n} de {total}",
|
||||
close: "Cerrar",
|
||||
},
|
||||
ticket: {
|
||||
title: 'Tu boleto',
|
||||
loading: 'Cargando boleto…',
|
||||
notFound: 'Boleto no encontrado',
|
||||
event: 'Evento',
|
||||
holder: 'Nombre',
|
||||
paid: 'Pagado',
|
||||
pending: 'Pago pendiente',
|
||||
registered: 'Registrado',
|
||||
showAtDoor: 'Muestra este QR en la entrada.',
|
||||
backToEvent: 'Volver al evento',
|
||||
title: "Tu boleto",
|
||||
loading: "Cargando boleto…",
|
||||
notFound: "Boleto no encontrado",
|
||||
event: "Evento",
|
||||
holder: "Nombre",
|
||||
paid: "Pagado",
|
||||
pending: "Pago pendiente",
|
||||
registered: "Registrado",
|
||||
showAtDoor: "Muestra este QR en la entrada.",
|
||||
backToEvent: "Volver al evento",
|
||||
},
|
||||
nav: {
|
||||
feed: 'Inicio',
|
||||
calendar: 'Calendario',
|
||||
map: 'Mapa',
|
||||
favorites: 'Favoritos',
|
||||
settings: 'Ajustes',
|
||||
feed: "Inicio",
|
||||
calendar: "Calendario",
|
||||
map: "Mapa",
|
||||
favorites: "Favoritos",
|
||||
settings: "Ajustes",
|
||||
},
|
||||
search: {
|
||||
placeholder: 'Buscar eventos...',
|
||||
noResults: 'No se encontraron eventos',
|
||||
placeholder: "Buscar eventos...",
|
||||
noResults: "No se encontraron eventos",
|
||||
},
|
||||
favorites: {
|
||||
title: 'Favoritos',
|
||||
loginPrompt: 'Inicia sesión para guardar tus eventos favoritos',
|
||||
empty: 'Aún no tienes favoritos',
|
||||
emptyHint: 'Toca el corazón en cualquier evento para guardarlo aquí',
|
||||
logIn: 'Iniciar sesión',
|
||||
title: "Favoritos",
|
||||
loginPrompt: "Inicia sesión para guardar tus eventos favoritos",
|
||||
empty: "Aún no tienes favoritos",
|
||||
emptyHint: "Toca el corazón en cualquier evento para guardarlo aquí",
|
||||
logIn: "Iniciar sesión",
|
||||
},
|
||||
settings: {
|
||||
title: 'Ajustes',
|
||||
account: 'Cuenta',
|
||||
loginPrompt: 'Inicia sesión para guardar eventos, confirmar asistencia y comprar boletos.',
|
||||
logIn: 'Iniciar sesión',
|
||||
logOut: 'Cerrar sesión',
|
||||
appearance: 'Apariencia',
|
||||
theme: 'Tema',
|
||||
language: 'Idioma',
|
||||
title: "Ajustes",
|
||||
account: "Cuenta",
|
||||
loginPrompt:
|
||||
"Inicia sesión para guardar eventos, confirmar asistencia y comprar boletos.",
|
||||
logIn: "Iniciar sesión",
|
||||
logOut: "Cerrar sesión",
|
||||
appearance: "Apariencia",
|
||||
theme: "Tema",
|
||||
language: "Idioma",
|
||||
},
|
||||
},
|
||||
libra: {
|
||||
nav: {
|
||||
record: 'Registrar',
|
||||
transactions: 'Transacciones',
|
||||
balance: 'Saldo',
|
||||
wallet: 'Billetera',
|
||||
settings: 'Ajustes',
|
||||
record: "Registrar",
|
||||
transactions: "Transacciones",
|
||||
balance: "Saldo",
|
||||
wallet: "Billetera",
|
||||
settings: "Ajustes",
|
||||
},
|
||||
record: {
|
||||
title: 'Registrar',
|
||||
addExpense: 'A\u00f1adir gasto',
|
||||
addExpenseDescription: 'Enviar un gasto para aprobaci\u00f3n',
|
||||
addIncome: 'A\u00f1adir ingreso',
|
||||
addIncomeDescription: 'Registrar un ingreso recibido',
|
||||
comingSoon: 'Pr\u00f3ximamente',
|
||||
drafts: 'Borradores',
|
||||
noDrafts: 'Sin borradores',
|
||||
draftAge: 'Guardado {time}',
|
||||
saveDraft: 'Guardar borrador',
|
||||
deleteDraft: 'Eliminar borrador',
|
||||
title: "Registrar",
|
||||
addExpense: "A\u00f1adir gasto",
|
||||
addExpenseDescription: "Enviar un gasto para aprobaci\u00f3n",
|
||||
addIncome: "A\u00f1adir ingreso",
|
||||
addIncomeDescription: "Registrar un ingreso recibido",
|
||||
comingSoon: "Pr\u00f3ximamente",
|
||||
drafts: "Borradores",
|
||||
noDrafts: "Sin borradores",
|
||||
draftAge: "Guardado {time}",
|
||||
saveDraft: "Guardar borrador",
|
||||
deleteDraft: "Eliminar borrador",
|
||||
},
|
||||
balance: {
|
||||
title: 'Saldo',
|
||||
netBalance: 'Saldo neto',
|
||||
pending: 'Pendiente',
|
||||
pendingExpenses: '{count} gasto pendiente | {count} gastos pendientes',
|
||||
pendingAmount: '{amount} pendiente de aprobaci\u00f3n',
|
||||
noBalance: 'No hay datos de saldo disponibles',
|
||||
owedToYou: 'Te deben',
|
||||
youOwe: 'Debes',
|
||||
title: "Saldo",
|
||||
netBalance: "Saldo neto",
|
||||
pending: "Pendiente",
|
||||
pendingExpenses: "{count} gasto pendiente | {count} gastos pendientes",
|
||||
pendingAmount: "{amount} pendiente de aprobaci\u00f3n",
|
||||
noBalance: "No hay datos de saldo disponibles",
|
||||
owedToYou: "Te deben",
|
||||
youOwe: "Debes",
|
||||
},
|
||||
settings: {
|
||||
title: 'Ajustes',
|
||||
account: 'Cuenta',
|
||||
loginPrompt: 'Inicia sesi\u00f3n para registrar gastos y ver tu saldo.',
|
||||
logIn: 'Iniciar sesi\u00f3n',
|
||||
logOut: 'Cerrar sesi\u00f3n',
|
||||
appearance: 'Apariencia',
|
||||
theme: 'Tema',
|
||||
language: 'Idioma',
|
||||
title: "Ajustes",
|
||||
account: "Cuenta",
|
||||
loginPrompt: "Inicia sesi\u00f3n para registrar gastos y ver tu saldo.",
|
||||
logIn: "Iniciar sesi\u00f3n",
|
||||
logOut: "Cerrar sesi\u00f3n",
|
||||
appearance: "Apariencia",
|
||||
theme: "Tema",
|
||||
language: "Idioma",
|
||||
},
|
||||
income: {
|
||||
title: 'A\u00f1adir ingreso',
|
||||
description: 'Enviar un ingreso para la organizaci\u00f3n',
|
||||
selectAccount: 'Seleccionar la cuenta de ingresos.',
|
||||
title: "A\u00f1adir ingreso",
|
||||
description: "Enviar un ingreso para la organizaci\u00f3n",
|
||||
selectAccount: "Seleccionar la cuenta de ingresos.",
|
||||
otherAccountHint: 'Use la cuenta "Other" si no está seguro.',
|
||||
submitIncome: 'Enviar ingreso',
|
||||
notAvailable: 'El registro de ingresos a\u00fan no est\u00e1 disponible. Esta funci\u00f3n llegar\u00e1 pronto.',
|
||||
submitIncome: "Enviar ingreso",
|
||||
notAvailable:
|
||||
"El registro de ingresos a\u00fan no est\u00e1 disponible. Esta funci\u00f3n llegar\u00e1 pronto.",
|
||||
},
|
||||
},
|
||||
market: {
|
||||
auth: {
|
||||
loginPrompt: 'Inicia sesi\u00f3n para realizar tu pedido',
|
||||
logIn: 'Iniciar sesi\u00f3n',
|
||||
logInToCheckout: 'Iniciar sesi\u00f3n para finalizar compra',
|
||||
nostrKeyRequired: 'Se requiere una identidad Nostr',
|
||||
nostrKeyDescription: 'Configura tu clave p\u00fablica Nostr en los ajustes del perfil para realizar pedidos.',
|
||||
loginPrompt: "Inicia sesi\u00f3n para realizar tu pedido",
|
||||
logIn: "Iniciar sesi\u00f3n",
|
||||
logInToCheckout: "Iniciar sesi\u00f3n para finalizar compra",
|
||||
nostrKeyRequired: "Se requiere una identidad Nostr",
|
||||
nostrKeyDescription:
|
||||
"Configura tu clave p\u00fablica Nostr en los ajustes del perfil para realizar pedidos.",
|
||||
},
|
||||
},
|
||||
dateTimeFormats: {
|
||||
short: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
},
|
||||
long: {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
}
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
weekday: "long",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
},
|
||||
},
|
||||
numberFormats: {
|
||||
currency: {
|
||||
style: 'currency',
|
||||
currencyDisplay: 'symbol'
|
||||
style: "currency",
|
||||
currencyDisplay: "symbol",
|
||||
},
|
||||
decimal: {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: 2
|
||||
style: "decimal",
|
||||
minimumFractionDigits: 2,
|
||||
},
|
||||
percent: {
|
||||
style: 'percent',
|
||||
useGrouping: false
|
||||
}
|
||||
}
|
||||
}
|
||||
style: "percent",
|
||||
useGrouping: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default messages
|
||||
export default messages;
|
||||
|
|
|
|||
|
|
@ -1,331 +1,346 @@
|
|||
import type { LocaleMessages } from '../types'
|
||||
import type { LocaleMessages } from "../types";
|
||||
|
||||
const messages: LocaleMessages = {
|
||||
nav: {
|
||||
title: 'Monde Virtuel',
|
||||
home: 'Accueil',
|
||||
directory: 'Répertoire',
|
||||
faq: 'FAQ',
|
||||
events: 'Événements',
|
||||
market: 'Marché',
|
||||
chat: 'Chat',
|
||||
login: 'Connexion',
|
||||
logout: 'Déconnexion'
|
||||
title: "Monde Virtuel",
|
||||
home: "Accueil",
|
||||
directory: "Répertoire",
|
||||
faq: "FAQ",
|
||||
events: "Événements",
|
||||
market: "Marché",
|
||||
chat: "Chat",
|
||||
login: "Connexion",
|
||||
logout: "Déconnexion",
|
||||
},
|
||||
common: {
|
||||
loading: 'Chargement...',
|
||||
error: 'Une erreur est survenue',
|
||||
success: 'Opération réussie',
|
||||
loading: "Chargement...",
|
||||
error: "Une erreur est survenue",
|
||||
success: "Opération réussie",
|
||||
nav: {
|
||||
profile: 'Profil',
|
||||
preferences: 'Préférences',
|
||||
profileDescription: 'Votre identité Nostr et votre nom d\u2019affichage.',
|
||||
profileLoggedOutDescription: 'Connectez-vous ou modifiez vos préférences.',
|
||||
login: 'Se connecter',
|
||||
menu: 'Menu',
|
||||
backToHub: 'Retour au hub',
|
||||
hub: 'Hub',
|
||||
theme: 'Thème',
|
||||
themeLight: 'Clair',
|
||||
themeDark: 'Sombre',
|
||||
themeSystem: 'Système',
|
||||
colorScheme: 'Palette',
|
||||
palette_catppuccin: 'Catppuccin',
|
||||
palette_countrysidecastle: 'Château Champêtre',
|
||||
palette_darkmatter: 'Dark Matter',
|
||||
palette_emeraldforest: 'Forêt d\'Émeraude',
|
||||
palette_lightgreen: 'Vert Clair',
|
||||
palette_neobrut: 'Néo-Brutaliste',
|
||||
palette_starrynight: 'Nuit Étoilée',
|
||||
language: 'Langue',
|
||||
currency: 'Devise',
|
||||
currencyComingSoon: 'Sélecteur de devise — bientôt disponible',
|
||||
currencyComingSoonDescription: 'Un réglage de devise préférée (sats/USD/EUR) est prévu.'
|
||||
}
|
||||
profile: "Profil",
|
||||
preferences: "Préférences",
|
||||
profileDescription: "Votre identité Nostr et votre nom d\u2019affichage.",
|
||||
profileLoggedOutDescription:
|
||||
"Connectez-vous ou modifiez vos préférences.",
|
||||
login: "Se connecter",
|
||||
menu: "Menu",
|
||||
backToHub: "Retour au hub",
|
||||
hub: "Hub",
|
||||
theme: "Thème",
|
||||
themeLight: "Clair",
|
||||
themeDark: "Sombre",
|
||||
themeSystem: "Système",
|
||||
colorScheme: "Palette",
|
||||
palette_catppuccin: "Catppuccin",
|
||||
palette_countrysidecastle: "Château Champêtre",
|
||||
palette_darkmatter: "Dark Matter",
|
||||
palette_emeraldforest: "Forêt d'Émeraude",
|
||||
palette_lightgreen: "Vert Clair",
|
||||
palette_neobrut: "Néo-Brutaliste",
|
||||
palette_starrynight: "Nuit Étoilée",
|
||||
language: "Langue",
|
||||
currency: "Devise",
|
||||
currencyComingSoon: "Sélecteur de devise — bientôt disponible",
|
||||
currencyComingSoonDescription:
|
||||
"Un réglage de devise préférée (sats/USD/EUR) est prévu.",
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
notFound: 'Page non trouvée',
|
||||
serverError: 'Erreur du serveur',
|
||||
networkError: 'Erreur de connexion réseau'
|
||||
notFound: "Page non trouvée",
|
||||
serverError: "Erreur du serveur",
|
||||
networkError: "Erreur de connexion réseau",
|
||||
},
|
||||
locales: {
|
||||
en: 'Anglais',
|
||||
es: 'Espagnol',
|
||||
fr: 'Français',
|
||||
de: 'Allemand',
|
||||
zh: 'Chinois'
|
||||
en: "Anglais",
|
||||
es: "Espagnol",
|
||||
fr: "Français",
|
||||
de: "Allemand",
|
||||
zh: "Chinois",
|
||||
},
|
||||
events: {
|
||||
title: 'Événements',
|
||||
createNew: 'Créer un événement',
|
||||
title: "Événements",
|
||||
createNew: "Créer un événement",
|
||||
create: {
|
||||
promo: {
|
||||
title: 'Codes promo',
|
||||
hint: 'Remises en pourcentage que les acheteurs peuvent saisir au paiement. Les codes ne sont jamais affichés publiquement.',
|
||||
empty: 'Aucun code promo pour le moment.',
|
||||
code: 'Code',
|
||||
codePlaceholder: 'EARLYBIRD',
|
||||
discount: 'Remise %',
|
||||
maxUses: 'Utilisations max.',
|
||||
active: 'Actif',
|
||||
used: 'Utilisé {count}',
|
||||
add: 'Ajouter un code',
|
||||
removeLabel: 'Supprimer le code {code}',
|
||||
title: "Codes promo",
|
||||
hint: "Remises en pourcentage que les acheteurs peuvent saisir au paiement. Les codes ne sont jamais affichés publiquement.",
|
||||
empty: "Aucun code promo pour le moment.",
|
||||
code: "Code",
|
||||
codePlaceholder: "EARLYBIRD",
|
||||
discount: "Remise %",
|
||||
maxUses: "Utilisations max.",
|
||||
active: "Actif",
|
||||
used: "Utilisé {count}",
|
||||
add: "Ajouter un code",
|
||||
removeLabel: "Supprimer le code {code}",
|
||||
errors: {
|
||||
required: 'Saisissez un code',
|
||||
duplicate: 'Code en double',
|
||||
percentRange: 'Entre 1 et 100',
|
||||
maxUsesMin: 'Doit être 1 ou plus',
|
||||
required: "Saisissez un code",
|
||||
duplicate: "Code en double",
|
||||
percentRange: "Entre 1 et 100",
|
||||
maxUsesMin: "Doit être 1 ou plus",
|
||||
},
|
||||
},
|
||||
},
|
||||
noEvents: 'Aucun événement trouvé',
|
||||
noEvents: "Aucun événement trouvé",
|
||||
filters: {
|
||||
all: 'Tout',
|
||||
all: "Tout",
|
||||
today: "Aujourd'hui",
|
||||
tomorrow: 'Demain',
|
||||
thisWeek: 'Cette semaine',
|
||||
thisMonth: 'Ce mois-ci',
|
||||
myTickets: 'Mes billets',
|
||||
hosting: 'J\'organise',
|
||||
pastEvents: 'Événements passés',
|
||||
past: 'Passé',
|
||||
filters: 'Filtres',
|
||||
clearAll: 'Tout effacer',
|
||||
filteringBy: 'Filtré par :',
|
||||
removeCategory: 'Retirer le filtre {category}',
|
||||
tomorrow: "Demain",
|
||||
thisWeek: "Cette semaine",
|
||||
thisMonth: "Ce mois-ci",
|
||||
myTickets: "Mes billets",
|
||||
hosting: "J'organise",
|
||||
pastEvents: "Événements passés",
|
||||
past: "Passé",
|
||||
filters: "Filtres",
|
||||
clearAll: "Tout effacer",
|
||||
filteringBy: "Filtré par :",
|
||||
removeCategory: "Retirer le filtre {category}",
|
||||
},
|
||||
categories: {
|
||||
concert: 'Concert',
|
||||
workshop: 'Atelier',
|
||||
market: 'Marché',
|
||||
festival: 'Festival',
|
||||
exhibition: 'Exposition',
|
||||
sport: 'Sport',
|
||||
theater: 'Théâtre',
|
||||
cinema: 'Cinéma',
|
||||
party: 'Fête',
|
||||
talk: 'Conférence',
|
||||
conference: 'Congrès',
|
||||
meetup: 'Rencontre',
|
||||
food: 'Gastronomie',
|
||||
outdoor: 'Plein air',
|
||||
kids: 'Enfants',
|
||||
wellness: 'Bien-être',
|
||||
technology: 'Technologie',
|
||||
art: 'Art',
|
||||
music: 'Musique',
|
||||
dance: 'Danse',
|
||||
literature: 'Littérature',
|
||||
comedy: 'Humour',
|
||||
charity: 'Caritatif',
|
||||
tradition: 'Tradition',
|
||||
other: 'Autre',
|
||||
concert: "Concert",
|
||||
workshop: "Atelier",
|
||||
market: "Marché",
|
||||
festival: "Festival",
|
||||
exhibition: "Exposition",
|
||||
sport: "Sport",
|
||||
theater: "Théâtre",
|
||||
cinema: "Cinéma",
|
||||
party: "Fête",
|
||||
talk: "Conférence",
|
||||
conference: "Congrès",
|
||||
meetup: "Rencontre",
|
||||
food: "Gastronomie",
|
||||
outdoor: "Plein air",
|
||||
kids: "Enfants",
|
||||
wellness: "Bien-être",
|
||||
technology: "Technologie",
|
||||
art: "Art",
|
||||
music: "Musique",
|
||||
dance: "Danse",
|
||||
literature: "Littérature",
|
||||
comedy: "Humour",
|
||||
charity: "Caritatif",
|
||||
tradition: "Tradition",
|
||||
other: "Autre",
|
||||
},
|
||||
detail: {
|
||||
getTicket: 'Obtenir un billet',
|
||||
getTicket: "Obtenir un billet",
|
||||
contactOrganizer: "Contacter l'organisateur",
|
||||
organizer: 'Organisateur',
|
||||
location: 'Lieu',
|
||||
when: 'Quand',
|
||||
tickets: 'Billets',
|
||||
ticketsAvailable: '{count} billets disponibles',
|
||||
ticketsRemainingOfTotal: '{count} sur {total} billets restants',
|
||||
ticketsOwned: 'Vous avez {count} billet | Vous avez {count} billets',
|
||||
unlimitedTickets: 'Billets illimités',
|
||||
buyTicket: 'Acheter un billet',
|
||||
buyAnotherTicket: 'Acheter un autre billet',
|
||||
viewMyTickets: 'Voir dans Mes billets',
|
||||
soldOut: 'Épuisé',
|
||||
pastEvent: 'Cet événement est déjà passé',
|
||||
loginToBuyTickets: 'Connectez-vous pour acheter des billets',
|
||||
logIn: 'Connexion',
|
||||
free: 'Gratuit',
|
||||
organizer: "Organisateur",
|
||||
location: "Lieu",
|
||||
when: "Quand",
|
||||
tickets: "Billets",
|
||||
ticketsAvailable: "{count} billets disponibles",
|
||||
ticketsOwned: "Vous avez {count} billet | Vous avez {count} billets",
|
||||
unlimitedTickets: "Billets illimités",
|
||||
buyTicket: "Acheter un billet",
|
||||
buyAnotherTicket: "Acheter un autre billet",
|
||||
viewMyTickets: "Voir dans Mes billets",
|
||||
soldOut: "Épuisé",
|
||||
pastEvent: "Cet événement est déjà passé",
|
||||
loginToBuyTickets: "Connectez-vous pour acheter des billets",
|
||||
logIn: "Connexion",
|
||||
free: "Gratuit",
|
||||
},
|
||||
tickets: {
|
||||
myTickets: 'Mes billets',
|
||||
scanTicket: 'Scanner le billet',
|
||||
noTickets: 'Pas encore de billets',
|
||||
paid: 'Payé',
|
||||
pending: 'En attente',
|
||||
registered: 'Enregistré',
|
||||
myTickets: "Mes billets",
|
||||
scanTicket: "Scanner le billet",
|
||||
noTickets: "Pas encore de billets",
|
||||
paid: "Payé",
|
||||
pending: "En attente",
|
||||
registered: "Enregistré",
|
||||
},
|
||||
purchase: {
|
||||
card: 'Carte',
|
||||
continueTo: 'Continuer vers le paiement {provider}',
|
||||
fiatRedirectHint: 'Vous serez redirigé vers le paiement sécurisé du prestataire, puis ramené ici une fois payé.',
|
||||
card: "Carte",
|
||||
continueTo: "Continuer vers le paiement {provider}",
|
||||
fiatRedirectHint:
|
||||
"Vous serez redirigé vers le paiement sécurisé du prestataire, puis ramené ici une fois payé.",
|
||||
noCheckoutUrl: "Le prestataire n'a pas renvoyé d'URL de paiement.",
|
||||
fiatFailed: 'Le paiement par carte a échoué',
|
||||
loginRequiredTitle: 'Connexion requise',
|
||||
loginRequiredFree: 'Les billets gratuits nécessitent un compte pour éviter les réservations anonymes. Connectez-vous pour obtenir le vôtre.',
|
||||
fiatFailed: "Le paiement par carte a échoué",
|
||||
loginRequiredTitle: "Connexion requise",
|
||||
loginRequiredFree:
|
||||
"Les billets gratuits nécessitent un compte pour éviter les réservations anonymes. Connectez-vous pour obtenir le vôtre.",
|
||||
promo: {
|
||||
label: 'Code promo',
|
||||
placeholder: 'Saisir le code',
|
||||
apply: 'Appliquer',
|
||||
remove: 'Retirer',
|
||||
checking: 'Vérification…',
|
||||
applied: '{code} −{percent}%',
|
||||
discountLine: 'Remise ({code})',
|
||||
label: "Code promo",
|
||||
placeholder: "Saisir le code",
|
||||
apply: "Appliquer",
|
||||
remove: "Retirer",
|
||||
checking: "Vérification…",
|
||||
applied: "{code} −{percent}%",
|
||||
discountLine: "Remise ({code})",
|
||||
invalid: "Ce code n'est pas valable pour cet achat.",
|
||||
unverified: "Impossible de vérifier le code — il sera appliqué au paiement s'il est valable.",
|
||||
loginRequiredDiscount: 'Ce code rend votre billet gratuit. Les billets gratuits nécessitent un compte : connectez-vous pour le réclamer.',
|
||||
unverified:
|
||||
"Impossible de vérifier le code — il sera appliqué au paiement s'il est valable.",
|
||||
loginRequiredDiscount:
|
||||
"Ce code rend votre billet gratuit. Les billets gratuits nécessitent un compte : connectez-vous pour le réclamer.",
|
||||
},
|
||||
contact: {
|
||||
title: 'Vos coordonnées',
|
||||
name: 'Nom',
|
||||
namePlaceholder: 'Nom sur le billet',
|
||||
nameRequired: 'Le nom est obligatoire',
|
||||
email: 'E-mail',
|
||||
emailHint: 'Votre billet sera envoyé à cette adresse.',
|
||||
spamHint: 'S’il n’est pas arrivé quelques minutes après le paiement, vérifiez votre dossier spam.',
|
||||
emailInvalid: 'Saisissez une adresse e-mail valide',
|
||||
title: "Vos coordonnées",
|
||||
name: "Nom",
|
||||
namePlaceholder: "Nom sur le billet",
|
||||
nameRequired: "Le nom est obligatoire",
|
||||
email: "E-mail",
|
||||
emailHint: "Votre billet sera envoyé à cette adresse.",
|
||||
spamHint:
|
||||
"S’il n’est pas arrivé quelques minutes après le paiement, vérifiez votre dossier spam.",
|
||||
emailInvalid: "Saisissez une adresse e-mail valide",
|
||||
confirmEmail: "Confirmer l'e-mail",
|
||||
emailMismatch: 'Les adresses e-mail ne correspondent pas',
|
||||
emailMismatch: "Les adresses e-mail ne correspondent pas",
|
||||
},
|
||||
},
|
||||
checkout: {
|
||||
title: 'Confirmation de votre paiement…',
|
||||
paidTitle: 'Paiement reçu',
|
||||
processing: 'En attente du règlement du paiement. Cela prend généralement quelques secondes.',
|
||||
stillProcessing: 'Toujours en cours. Votre billet sera envoyé à {email} dès que le paiement sera réglé.',
|
||||
stillProcessingNoEmail: 'Toujours en cours. Rechargez cette page dans un instant.',
|
||||
emailedTo: 'Nous avons envoyé votre billet à {email}.',
|
||||
cancelled: 'Paiement annulé — aucune place réservée.',
|
||||
viewTicket: 'Ouvrir la page du billet',
|
||||
viewMyTickets: 'Voir mes billets',
|
||||
retry: 'Vérifier à nouveau',
|
||||
ticketN: 'Billet {n} sur {total}',
|
||||
close: 'Fermer',
|
||||
title: "Confirmation de votre paiement…",
|
||||
paidTitle: "Paiement reçu",
|
||||
processing:
|
||||
"En attente du règlement du paiement. Cela prend généralement quelques secondes.",
|
||||
stillProcessing:
|
||||
"Toujours en cours. Votre billet sera envoyé à {email} dès que le paiement sera réglé.",
|
||||
stillProcessingNoEmail:
|
||||
"Toujours en cours. Rechargez cette page dans un instant.",
|
||||
emailedTo: "Nous avons envoyé votre billet à {email}.",
|
||||
cancelled: "Paiement annulé — aucune place réservée.",
|
||||
viewTicket: "Ouvrir la page du billet",
|
||||
viewMyTickets: "Voir mes billets",
|
||||
retry: "Vérifier à nouveau",
|
||||
ticketN: "Billet {n} sur {total}",
|
||||
close: "Fermer",
|
||||
},
|
||||
ticket: {
|
||||
title: 'Votre billet',
|
||||
loading: 'Chargement du billet…',
|
||||
notFound: 'Billet introuvable',
|
||||
event: 'Événement',
|
||||
holder: 'Nom',
|
||||
paid: 'Payé',
|
||||
pending: 'Paiement en attente',
|
||||
registered: 'Enregistré',
|
||||
title: "Votre billet",
|
||||
loading: "Chargement du billet…",
|
||||
notFound: "Billet introuvable",
|
||||
event: "Événement",
|
||||
holder: "Nom",
|
||||
paid: "Payé",
|
||||
pending: "Paiement en attente",
|
||||
registered: "Enregistré",
|
||||
showAtDoor: "Présentez ce QR à l'entrée.",
|
||||
backToEvent: "Retour à l'événement",
|
||||
},
|
||||
nav: {
|
||||
feed: 'Accueil',
|
||||
calendar: 'Calendrier',
|
||||
map: 'Carte',
|
||||
favorites: 'Favoris',
|
||||
settings: 'Réglages',
|
||||
feed: "Accueil",
|
||||
calendar: "Calendrier",
|
||||
map: "Carte",
|
||||
favorites: "Favoris",
|
||||
settings: "Réglages",
|
||||
},
|
||||
search: {
|
||||
placeholder: 'Rechercher des événements...',
|
||||
noResults: 'Aucun événement trouvé',
|
||||
placeholder: "Rechercher des événements...",
|
||||
noResults: "Aucun événement trouvé",
|
||||
},
|
||||
favorites: {
|
||||
title: 'Favoris',
|
||||
loginPrompt: 'Connectez-vous pour sauvegarder vos événements préférés',
|
||||
empty: 'Pas encore de favoris',
|
||||
title: "Favoris",
|
||||
loginPrompt: "Connectez-vous pour sauvegarder vos événements préférés",
|
||||
empty: "Pas encore de favoris",
|
||||
emptyHint: "Appuyez sur le cœur d'un événement pour le sauvegarder ici",
|
||||
logIn: 'Se connecter',
|
||||
logIn: "Se connecter",
|
||||
},
|
||||
settings: {
|
||||
title: 'Réglages',
|
||||
account: 'Compte',
|
||||
loginPrompt: 'Connectez-vous pour sauvegarder des événements, confirmer votre présence et acheter des billets.',
|
||||
logIn: 'Se connecter',
|
||||
logOut: 'Se déconnecter',
|
||||
appearance: 'Apparence',
|
||||
theme: 'Thème',
|
||||
language: 'Langue',
|
||||
title: "Réglages",
|
||||
account: "Compte",
|
||||
loginPrompt:
|
||||
"Connectez-vous pour sauvegarder des événements, confirmer votre présence et acheter des billets.",
|
||||
logIn: "Se connecter",
|
||||
logOut: "Se déconnecter",
|
||||
appearance: "Apparence",
|
||||
theme: "Thème",
|
||||
language: "Langue",
|
||||
},
|
||||
},
|
||||
libra: {
|
||||
nav: {
|
||||
record: 'Saisir',
|
||||
transactions: 'Transactions',
|
||||
balance: 'Solde',
|
||||
wallet: 'Portefeuille',
|
||||
settings: 'Param\u00e8tres',
|
||||
record: "Saisir",
|
||||
transactions: "Transactions",
|
||||
balance: "Solde",
|
||||
wallet: "Portefeuille",
|
||||
settings: "Param\u00e8tres",
|
||||
},
|
||||
record: {
|
||||
title: 'Saisir',
|
||||
addExpense: 'Ajouter une d\u00e9pense',
|
||||
addExpenseDescription: 'Soumettre une d\u00e9pense pour approbation',
|
||||
addIncome: 'Ajouter un revenu',
|
||||
addIncomeDescription: 'Enregistrer un revenu re\u00e7u',
|
||||
comingSoon: 'Bient\u00f4t disponible',
|
||||
drafts: 'Brouillons',
|
||||
noDrafts: 'Aucun brouillon',
|
||||
draftAge: 'Enregistr\u00e9 {time}',
|
||||
saveDraft: 'Enregistrer le brouillon',
|
||||
deleteDraft: 'Supprimer le brouillon',
|
||||
title: "Saisir",
|
||||
addExpense: "Ajouter une d\u00e9pense",
|
||||
addExpenseDescription: "Soumettre une d\u00e9pense pour approbation",
|
||||
addIncome: "Ajouter un revenu",
|
||||
addIncomeDescription: "Enregistrer un revenu re\u00e7u",
|
||||
comingSoon: "Bient\u00f4t disponible",
|
||||
drafts: "Brouillons",
|
||||
noDrafts: "Aucun brouillon",
|
||||
draftAge: "Enregistr\u00e9 {time}",
|
||||
saveDraft: "Enregistrer le brouillon",
|
||||
deleteDraft: "Supprimer le brouillon",
|
||||
},
|
||||
balance: {
|
||||
title: 'Solde',
|
||||
netBalance: 'Solde net',
|
||||
pending: 'En attente',
|
||||
pendingExpenses: '{count} d\u00e9pense en attente | {count} d\u00e9penses en attente',
|
||||
pendingAmount: '{amount} en attente d\u2019approbation',
|
||||
noBalance: 'Aucune donn\u00e9e de solde disponible',
|
||||
owedToYou: 'D\u00fb',
|
||||
youOwe: 'Vous devez',
|
||||
title: "Solde",
|
||||
netBalance: "Solde net",
|
||||
pending: "En attente",
|
||||
pendingExpenses:
|
||||
"{count} d\u00e9pense en attente | {count} d\u00e9penses en attente",
|
||||
pendingAmount: "{amount} en attente d\u2019approbation",
|
||||
noBalance: "Aucune donn\u00e9e de solde disponible",
|
||||
owedToYou: "D\u00fb",
|
||||
youOwe: "Vous devez",
|
||||
},
|
||||
settings: {
|
||||
title: 'Param\u00e8tres',
|
||||
account: 'Compte',
|
||||
loginPrompt: 'Connectez-vous pour saisir des d\u00e9penses et voir votre solde.',
|
||||
logIn: 'Se connecter',
|
||||
logOut: 'Se d\u00e9connecter',
|
||||
appearance: 'Apparence',
|
||||
theme: 'Th\u00e8me',
|
||||
language: 'Langue',
|
||||
title: "Param\u00e8tres",
|
||||
account: "Compte",
|
||||
loginPrompt:
|
||||
"Connectez-vous pour saisir des d\u00e9penses et voir votre solde.",
|
||||
logIn: "Se connecter",
|
||||
logOut: "Se d\u00e9connecter",
|
||||
appearance: "Apparence",
|
||||
theme: "Th\u00e8me",
|
||||
language: "Langue",
|
||||
},
|
||||
income: {
|
||||
title: 'Ajouter un revenu',
|
||||
description: 'Soumettre un revenu pour l\u2019organisation',
|
||||
selectAccount: 'S\u00e9lectionner le compte de revenus.',
|
||||
otherAccountHint: 'Utilisez le compte "Other" si vous n\'\u00eates pas s\u00fbr.',
|
||||
submitIncome: 'Soumettre le revenu',
|
||||
notAvailable: 'La saisie de revenus n\u2019est pas encore disponible. Cette fonctionnalit\u00e9 arrive bient\u00f4t.',
|
||||
title: "Ajouter un revenu",
|
||||
description: "Soumettre un revenu pour l\u2019organisation",
|
||||
selectAccount: "S\u00e9lectionner le compte de revenus.",
|
||||
otherAccountHint:
|
||||
'Utilisez le compte "Other" si vous n\'\u00eates pas s\u00fbr.',
|
||||
submitIncome: "Soumettre le revenu",
|
||||
notAvailable:
|
||||
"La saisie de revenus n\u2019est pas encore disponible. Cette fonctionnalit\u00e9 arrive bient\u00f4t.",
|
||||
},
|
||||
},
|
||||
market: {
|
||||
auth: {
|
||||
loginPrompt: 'Connectez-vous pour passer commande',
|
||||
logIn: 'Se connecter',
|
||||
logInToCheckout: 'Se connecter pour commander',
|
||||
nostrKeyRequired: 'Une identit\u00e9 Nostr est requise',
|
||||
nostrKeyDescription: 'Configurez votre cl\u00e9 publique Nostr dans les param\u00e8tres du profil pour passer commande.',
|
||||
loginPrompt: "Connectez-vous pour passer commande",
|
||||
logIn: "Se connecter",
|
||||
logInToCheckout: "Se connecter pour commander",
|
||||
nostrKeyRequired: "Une identit\u00e9 Nostr est requise",
|
||||
nostrKeyDescription:
|
||||
"Configurez votre cl\u00e9 publique Nostr dans les param\u00e8tres du profil pour passer commande.",
|
||||
},
|
||||
},
|
||||
dateTimeFormats: {
|
||||
short: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
},
|
||||
long: {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
}
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
weekday: "long",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
},
|
||||
},
|
||||
numberFormats: {
|
||||
currency: {
|
||||
style: 'currency',
|
||||
currencyDisplay: 'symbol'
|
||||
style: "currency",
|
||||
currencyDisplay: "symbol",
|
||||
},
|
||||
decimal: {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: 2
|
||||
style: "decimal",
|
||||
minimumFractionDigits: 2,
|
||||
},
|
||||
percent: {
|
||||
style: 'percent',
|
||||
useGrouping: false
|
||||
}
|
||||
}
|
||||
}
|
||||
style: "percent",
|
||||
useGrouping: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default messages
|
||||
export default messages;
|
||||
|
|
|
|||
|
|
@ -1,308 +1,307 @@
|
|||
export interface LocaleMessages {
|
||||
nav: {
|
||||
title: string
|
||||
home: string
|
||||
directory: string
|
||||
faq: string
|
||||
events: string
|
||||
market: string
|
||||
chat: string
|
||||
login: string
|
||||
logout: string
|
||||
}
|
||||
title: string;
|
||||
home: string;
|
||||
directory: string;
|
||||
faq: string;
|
||||
events: string;
|
||||
market: string;
|
||||
chat: string;
|
||||
login: string;
|
||||
logout: string;
|
||||
};
|
||||
// Add more message categories here
|
||||
common: {
|
||||
loading: string
|
||||
error: string
|
||||
success: string
|
||||
loading: string;
|
||||
error: string;
|
||||
success: string;
|
||||
nav: {
|
||||
profile: string
|
||||
preferences: string
|
||||
profileDescription: string
|
||||
profileLoggedOutDescription: string
|
||||
login: string
|
||||
menu: string
|
||||
backToHub: string
|
||||
hub: string
|
||||
theme: string
|
||||
themeLight: string
|
||||
themeDark: string
|
||||
themeSystem: string
|
||||
colorScheme: string
|
||||
palette_catppuccin: string
|
||||
palette_countrysidecastle: string
|
||||
palette_darkmatter: string
|
||||
palette_emeraldforest: string
|
||||
palette_lightgreen: string
|
||||
palette_neobrut: string
|
||||
palette_starrynight: string
|
||||
language: string
|
||||
currency: string
|
||||
currencyComingSoon: string
|
||||
currencyComingSoonDescription: string
|
||||
}
|
||||
}
|
||||
profile: string;
|
||||
preferences: string;
|
||||
profileDescription: string;
|
||||
profileLoggedOutDescription: string;
|
||||
login: string;
|
||||
menu: string;
|
||||
backToHub: string;
|
||||
hub: string;
|
||||
theme: string;
|
||||
themeLight: string;
|
||||
themeDark: string;
|
||||
themeSystem: string;
|
||||
colorScheme: string;
|
||||
palette_catppuccin: string;
|
||||
palette_countrysidecastle: string;
|
||||
palette_darkmatter: string;
|
||||
palette_emeraldforest: string;
|
||||
palette_lightgreen: string;
|
||||
palette_neobrut: string;
|
||||
palette_starrynight: string;
|
||||
language: string;
|
||||
currency: string;
|
||||
currencyComingSoon: string;
|
||||
currencyComingSoonDescription: string;
|
||||
};
|
||||
};
|
||||
errors: {
|
||||
notFound: string
|
||||
serverError: string
|
||||
networkError: string
|
||||
}
|
||||
notFound: string;
|
||||
serverError: string;
|
||||
networkError: string;
|
||||
};
|
||||
// Language names in the current language
|
||||
locales: {
|
||||
en: string
|
||||
es: string
|
||||
fr: string
|
||||
de: string
|
||||
zh: string
|
||||
}
|
||||
en: string;
|
||||
es: string;
|
||||
fr: string;
|
||||
de: string;
|
||||
zh: string;
|
||||
};
|
||||
// Events module
|
||||
events?: {
|
||||
title: string
|
||||
createNew: string
|
||||
title: string;
|
||||
createNew: string;
|
||||
create: {
|
||||
promo: {
|
||||
title: string
|
||||
hint: string
|
||||
empty: string
|
||||
code: string
|
||||
codePlaceholder: string
|
||||
discount: string
|
||||
maxUses: string
|
||||
active: string
|
||||
used: string
|
||||
add: string
|
||||
removeLabel: string
|
||||
title: string;
|
||||
hint: string;
|
||||
empty: string;
|
||||
code: string;
|
||||
codePlaceholder: string;
|
||||
discount: string;
|
||||
maxUses: string;
|
||||
active: string;
|
||||
used: string;
|
||||
add: string;
|
||||
removeLabel: string;
|
||||
errors: {
|
||||
required: string
|
||||
duplicate: string
|
||||
percentRange: string
|
||||
maxUsesMin: string
|
||||
}
|
||||
}
|
||||
}
|
||||
noEvents: string
|
||||
required: string;
|
||||
duplicate: string;
|
||||
percentRange: string;
|
||||
maxUsesMin: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
noEvents: string;
|
||||
filters: {
|
||||
all: string
|
||||
today: string
|
||||
tomorrow: string
|
||||
thisWeek: string
|
||||
thisMonth: string
|
||||
myTickets: string
|
||||
hosting: string
|
||||
pastEvents: string
|
||||
past: string
|
||||
filters: string
|
||||
clearAll: string
|
||||
filteringBy: string
|
||||
removeCategory: string
|
||||
}
|
||||
categories: Record<string, string>
|
||||
all: string;
|
||||
today: string;
|
||||
tomorrow: string;
|
||||
thisWeek: string;
|
||||
thisMonth: string;
|
||||
myTickets: string;
|
||||
hosting: string;
|
||||
pastEvents: string;
|
||||
past: string;
|
||||
filters: string;
|
||||
clearAll: string;
|
||||
filteringBy: string;
|
||||
removeCategory: string;
|
||||
};
|
||||
categories: Record<string, string>;
|
||||
detail: {
|
||||
getTicket: string
|
||||
contactOrganizer: string
|
||||
organizer: string
|
||||
location: string
|
||||
when: string
|
||||
tickets: string
|
||||
ticketsAvailable: string
|
||||
ticketsRemainingOfTotal: string
|
||||
ticketsOwned: string
|
||||
unlimitedTickets: string
|
||||
buyTicket: string
|
||||
buyAnotherTicket: string
|
||||
viewMyTickets: string
|
||||
soldOut: string
|
||||
pastEvent: string
|
||||
loginToBuyTickets: string
|
||||
logIn: string
|
||||
free: string
|
||||
}
|
||||
getTicket: string;
|
||||
contactOrganizer: string;
|
||||
organizer: string;
|
||||
location: string;
|
||||
when: string;
|
||||
tickets: string;
|
||||
ticketsAvailable: string;
|
||||
ticketsOwned: string;
|
||||
unlimitedTickets: string;
|
||||
buyTicket: string;
|
||||
buyAnotherTicket: string;
|
||||
viewMyTickets: string;
|
||||
soldOut: string;
|
||||
pastEvent: string;
|
||||
loginToBuyTickets: string;
|
||||
logIn: string;
|
||||
free: string;
|
||||
};
|
||||
tickets: {
|
||||
myTickets: string
|
||||
scanTicket: string
|
||||
noTickets: string
|
||||
paid: string
|
||||
pending: string
|
||||
registered: string
|
||||
}
|
||||
myTickets: string;
|
||||
scanTicket: string;
|
||||
noTickets: string;
|
||||
paid: string;
|
||||
pending: string;
|
||||
registered: string;
|
||||
};
|
||||
purchase: {
|
||||
card: string
|
||||
continueTo: string
|
||||
fiatRedirectHint: string
|
||||
noCheckoutUrl: string
|
||||
fiatFailed: string
|
||||
loginRequiredTitle: string
|
||||
loginRequiredFree: string
|
||||
card: string;
|
||||
continueTo: string;
|
||||
fiatRedirectHint: string;
|
||||
noCheckoutUrl: string;
|
||||
fiatFailed: string;
|
||||
loginRequiredTitle: string;
|
||||
loginRequiredFree: string;
|
||||
promo: {
|
||||
label: string
|
||||
placeholder: string
|
||||
apply: string
|
||||
remove: string
|
||||
checking: string
|
||||
applied: string
|
||||
discountLine: string
|
||||
invalid: string
|
||||
unverified: string
|
||||
loginRequiredDiscount: string
|
||||
}
|
||||
label: string;
|
||||
placeholder: string;
|
||||
apply: string;
|
||||
remove: string;
|
||||
checking: string;
|
||||
applied: string;
|
||||
discountLine: string;
|
||||
invalid: string;
|
||||
unverified: string;
|
||||
loginRequiredDiscount: string;
|
||||
};
|
||||
contact: {
|
||||
title: string
|
||||
name: string
|
||||
namePlaceholder: string
|
||||
nameRequired: string
|
||||
email: string
|
||||
emailHint: string
|
||||
spamHint: string
|
||||
emailInvalid: string
|
||||
confirmEmail: string
|
||||
emailMismatch: string
|
||||
}
|
||||
}
|
||||
title: string;
|
||||
name: string;
|
||||
namePlaceholder: string;
|
||||
nameRequired: string;
|
||||
email: string;
|
||||
emailHint: string;
|
||||
spamHint: string;
|
||||
emailInvalid: string;
|
||||
confirmEmail: string;
|
||||
emailMismatch: string;
|
||||
};
|
||||
};
|
||||
checkout: {
|
||||
title: string
|
||||
paidTitle: string
|
||||
processing: string
|
||||
stillProcessing: string
|
||||
stillProcessingNoEmail: string
|
||||
emailedTo: string
|
||||
cancelled: string
|
||||
viewTicket: string
|
||||
viewMyTickets: string
|
||||
retry: string
|
||||
ticketN: string
|
||||
close: string
|
||||
}
|
||||
title: string;
|
||||
paidTitle: string;
|
||||
processing: string;
|
||||
stillProcessing: string;
|
||||
stillProcessingNoEmail: string;
|
||||
emailedTo: string;
|
||||
cancelled: string;
|
||||
viewTicket: string;
|
||||
viewMyTickets: string;
|
||||
retry: string;
|
||||
ticketN: string;
|
||||
close: string;
|
||||
};
|
||||
ticket: {
|
||||
title: string
|
||||
loading: string
|
||||
notFound: string
|
||||
event: string
|
||||
holder: string
|
||||
paid: string
|
||||
pending: string
|
||||
registered: string
|
||||
showAtDoor: string
|
||||
backToEvent: string
|
||||
}
|
||||
title: string;
|
||||
loading: string;
|
||||
notFound: string;
|
||||
event: string;
|
||||
holder: string;
|
||||
paid: string;
|
||||
pending: string;
|
||||
registered: string;
|
||||
showAtDoor: string;
|
||||
backToEvent: string;
|
||||
};
|
||||
nav: {
|
||||
feed: string
|
||||
calendar: string
|
||||
map: string
|
||||
favorites: string
|
||||
settings: string
|
||||
}
|
||||
feed: string;
|
||||
calendar: string;
|
||||
map: string;
|
||||
favorites: string;
|
||||
settings: string;
|
||||
};
|
||||
search: {
|
||||
placeholder: string
|
||||
noResults: string
|
||||
}
|
||||
placeholder: string;
|
||||
noResults: string;
|
||||
};
|
||||
favorites: {
|
||||
title: string
|
||||
loginPrompt: string
|
||||
empty: string
|
||||
emptyHint: string
|
||||
logIn: string
|
||||
}
|
||||
title: string;
|
||||
loginPrompt: string;
|
||||
empty: string;
|
||||
emptyHint: string;
|
||||
logIn: string;
|
||||
};
|
||||
settings: {
|
||||
title: string
|
||||
account: string
|
||||
loginPrompt: string
|
||||
logIn: string
|
||||
logOut: string
|
||||
appearance: string
|
||||
theme: string
|
||||
language: string
|
||||
}
|
||||
}
|
||||
title: string;
|
||||
account: string;
|
||||
loginPrompt: string;
|
||||
logIn: string;
|
||||
logOut: string;
|
||||
appearance: string;
|
||||
theme: string;
|
||||
language: string;
|
||||
};
|
||||
};
|
||||
// Libra accounting module
|
||||
libra?: {
|
||||
nav: {
|
||||
record: string
|
||||
transactions: string
|
||||
balance: string
|
||||
wallet: string
|
||||
settings: string
|
||||
}
|
||||
record: string;
|
||||
transactions: string;
|
||||
balance: string;
|
||||
wallet: string;
|
||||
settings: string;
|
||||
};
|
||||
record: {
|
||||
title: string
|
||||
addExpense: string
|
||||
addExpenseDescription: string
|
||||
addIncome: string
|
||||
addIncomeDescription: string
|
||||
comingSoon: string
|
||||
drafts: string
|
||||
noDrafts: string
|
||||
draftAge: string
|
||||
saveDraft: string
|
||||
deleteDraft: string
|
||||
}
|
||||
title: string;
|
||||
addExpense: string;
|
||||
addExpenseDescription: string;
|
||||
addIncome: string;
|
||||
addIncomeDescription: string;
|
||||
comingSoon: string;
|
||||
drafts: string;
|
||||
noDrafts: string;
|
||||
draftAge: string;
|
||||
saveDraft: string;
|
||||
deleteDraft: string;
|
||||
};
|
||||
balance: {
|
||||
title: string
|
||||
netBalance: string
|
||||
pending: string
|
||||
pendingExpenses: string
|
||||
pendingAmount: string
|
||||
noBalance: string
|
||||
owedToYou: string
|
||||
youOwe: string
|
||||
}
|
||||
title: string;
|
||||
netBalance: string;
|
||||
pending: string;
|
||||
pendingExpenses: string;
|
||||
pendingAmount: string;
|
||||
noBalance: string;
|
||||
owedToYou: string;
|
||||
youOwe: string;
|
||||
};
|
||||
settings: {
|
||||
title: string
|
||||
account: string
|
||||
loginPrompt: string
|
||||
logIn: string
|
||||
logOut: string
|
||||
appearance: string
|
||||
theme: string
|
||||
language: string
|
||||
}
|
||||
title: string;
|
||||
account: string;
|
||||
loginPrompt: string;
|
||||
logIn: string;
|
||||
logOut: string;
|
||||
appearance: string;
|
||||
theme: string;
|
||||
language: string;
|
||||
};
|
||||
income: {
|
||||
title: string
|
||||
description: string
|
||||
selectAccount: string
|
||||
otherAccountHint: string
|
||||
submitIncome: string
|
||||
notAvailable: string
|
||||
}
|
||||
}
|
||||
title: string;
|
||||
description: string;
|
||||
selectAccount: string;
|
||||
otherAccountHint: string;
|
||||
submitIncome: string;
|
||||
notAvailable: string;
|
||||
};
|
||||
};
|
||||
// Market module
|
||||
market?: {
|
||||
auth: {
|
||||
loginPrompt: string
|
||||
logIn: string
|
||||
logInToCheckout: string
|
||||
nostrKeyRequired: string
|
||||
nostrKeyDescription: string
|
||||
}
|
||||
}
|
||||
loginPrompt: string;
|
||||
logIn: string;
|
||||
logInToCheckout: string;
|
||||
nostrKeyRequired: string;
|
||||
nostrKeyDescription: string;
|
||||
};
|
||||
};
|
||||
// Add date/time formats
|
||||
dateTimeFormats: {
|
||||
short: {
|
||||
year: 'numeric'
|
||||
month: 'short'
|
||||
day: 'numeric'
|
||||
}
|
||||
year: "numeric";
|
||||
month: "short";
|
||||
day: "numeric";
|
||||
};
|
||||
long: {
|
||||
year: 'numeric'
|
||||
month: 'long'
|
||||
day: 'numeric'
|
||||
weekday: 'long'
|
||||
hour: 'numeric'
|
||||
minute: 'numeric'
|
||||
}
|
||||
}
|
||||
year: "numeric";
|
||||
month: "long";
|
||||
day: "numeric";
|
||||
weekday: "long";
|
||||
hour: "numeric";
|
||||
minute: "numeric";
|
||||
};
|
||||
};
|
||||
// Add number formats
|
||||
numberFormats: {
|
||||
currency: {
|
||||
style: 'currency'
|
||||
currencyDisplay: 'symbol'
|
||||
}
|
||||
style: "currency";
|
||||
currencyDisplay: "symbol";
|
||||
};
|
||||
decimal: {
|
||||
style: 'decimal'
|
||||
minimumFractionDigits: 2
|
||||
}
|
||||
style: "decimal";
|
||||
minimumFractionDigits: 2;
|
||||
};
|
||||
percent: {
|
||||
style: 'percent'
|
||||
useGrouping: false
|
||||
}
|
||||
}
|
||||
style: "percent";
|
||||
useGrouping: false;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -1,74 +1,81 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { format } from 'date-fns'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { MapPin, Calendar, Ticket, User, CheckCircle2, History } from 'lucide-vue-next'
|
||||
import BookmarkButton from './BookmarkButton.vue'
|
||||
import OrganizerCard from './OrganizerCard.vue'
|
||||
import { useDateLocale } from '../composables/useDateLocale'
|
||||
import { useOwnedTickets } from '../composables/useOwnedTickets'
|
||||
import type { Event } from '../types/event'
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { format } from "date-fns";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
MapPin,
|
||||
Calendar,
|
||||
Ticket,
|
||||
User,
|
||||
CheckCircle2,
|
||||
History,
|
||||
} from "lucide-vue-next";
|
||||
import BookmarkButton from "./BookmarkButton.vue";
|
||||
import OrganizerCard from "./OrganizerCard.vue";
|
||||
import { useDateLocale } from "../composables/useDateLocale";
|
||||
import { useOwnedTickets } from "../composables/useOwnedTickets";
|
||||
import type { Event } from "../types/event";
|
||||
|
||||
const props = defineProps<{
|
||||
event: Event
|
||||
event: Event;
|
||||
/** Render a compact row: no hero image, no summary, single-line
|
||||
* title, tighter padding. Used by the Hosting view where the
|
||||
* host already knows what their events look like. */
|
||||
compact?: boolean
|
||||
}>()
|
||||
compact?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [event: Event]
|
||||
}>()
|
||||
click: [event: Event];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n()
|
||||
const { dateLocale } = useDateLocale()
|
||||
const { paidCount } = useOwnedTickets()
|
||||
const { t } = useI18n();
|
||||
const { dateLocale } = useDateLocale();
|
||||
const { paidCount } = useOwnedTickets();
|
||||
|
||||
const ownedCount = computed(() => paidCount(props.event.id))
|
||||
const ownedCount = computed(() => paidCount(props.event.id));
|
||||
|
||||
const dateDisplay = computed(() => {
|
||||
const a = props.event
|
||||
if (!a.startDate || isNaN(a.startDate.getTime())) return ''
|
||||
const a = props.event;
|
||||
if (!a.startDate || isNaN(a.startDate.getTime())) return "";
|
||||
try {
|
||||
const opts = { locale: dateLocale.value }
|
||||
if (a.type === 'date') {
|
||||
return format(a.startDate, 'EEE, MMM d', opts)
|
||||
const opts = { locale: dateLocale.value };
|
||||
if (a.type === "date") {
|
||||
return format(a.startDate, "EEE, MMM d", opts);
|
||||
}
|
||||
const date = format(a.startDate, 'EEE, MMM d', opts)
|
||||
const time = format(a.startDate, 'HH:mm', opts)
|
||||
return `${date} \u2022 ${time}`
|
||||
const date = format(a.startDate, "EEE, MMM d", opts);
|
||||
const time = format(a.startDate, "HH:mm", opts);
|
||||
return `${date} \u2022 ${time}`;
|
||||
} catch {
|
||||
return ''
|
||||
return "";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const categoryLabel = computed(() => {
|
||||
if (!props.event.category) return null
|
||||
return t(`events.categories.${props.event.category}`, props.event.category)
|
||||
})
|
||||
if (!props.event.category) return null;
|
||||
return t(`events.categories.${props.event.category}`, props.event.category);
|
||||
});
|
||||
|
||||
const priceDisplay = computed(() => {
|
||||
const info = props.event.ticketInfo
|
||||
if (!info) return null
|
||||
if (info.price === 0) return t('events.detail.free')
|
||||
return `${info.price} ${info.currency}`
|
||||
})
|
||||
const info = props.event.ticketInfo;
|
||||
if (!info) return null;
|
||||
if (info.price === 0) return t("events.detail.free");
|
||||
return `${info.price} ${info.currency}`;
|
||||
});
|
||||
|
||||
const isPast = computed(() => {
|
||||
const a = props.event
|
||||
const end = a.endDate ?? a.startDate
|
||||
if (!end || isNaN(end.getTime())) return false
|
||||
return end.getTime() < Date.now()
|
||||
})
|
||||
const a = props.event;
|
||||
const end = a.endDate ?? a.startDate;
|
||||
if (!end || isNaN(end.getTime())) return false;
|
||||
return end.getTime() < Date.now();
|
||||
});
|
||||
|
||||
// Pending / rejected events get a washed-out look so the user
|
||||
// sees at a glance the event isn't live, not just the small badge.
|
||||
const isNonApproved = computed(
|
||||
() => !!props.event.lnbitsStatus && props.event.lnbitsStatus !== 'approved',
|
||||
)
|
||||
() => !!props.event.lnbitsStatus && props.event.lnbitsStatus !== "approved",
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -92,187 +99,210 @@ const isNonApproved = computed(
|
|||
isNonApproved ? 'opacity-50 grayscale hover:opacity-90' : '',
|
||||
]"
|
||||
>
|
||||
<!-- Compact thumbnail — small square preview on the left of the
|
||||
<!-- Compact thumbnail — small square preview on the left of the
|
||||
row when the event carries an image. `self-center` keeps it
|
||||
vertically centered against a taller content column so we
|
||||
don't get a top-anchored thumb with dead space below. -->
|
||||
<img
|
||||
v-if="compact && event.image"
|
||||
:src="event.image"
|
||||
:alt="event.title"
|
||||
class="w-20 h-20 object-cover shrink-0 self-center ml-3 rounded-md"
|
||||
loading="lazy"
|
||||
/>
|
||||
<!-- Image with overlaid badges. Cards without an image (or in
|
||||
<img
|
||||
v-if="compact && event.image"
|
||||
:src="event.image"
|
||||
:alt="event.title"
|
||||
class="w-20 h-20 object-cover shrink-0 self-center ml-3 rounded-md"
|
||||
loading="lazy"
|
||||
/>
|
||||
<!-- Image with overlaid badges. Cards without an image (or in
|
||||
compact mode) skip the hero area entirely and surface their
|
||||
badges inline at the top of the content block — the solid-
|
||||
color placeholder + calendar glyph wasn't communicating
|
||||
anything the title + details don't already. -->
|
||||
<div v-if="event.image && !compact" class="relative aspect-[16/9] overflow-hidden rounded-t-lg">
|
||||
<img
|
||||
:src="event.image"
|
||||
:alt="event.title"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<!-- Category badge -->
|
||||
<Badge
|
||||
v-if="categoryLabel"
|
||||
variant="secondary"
|
||||
class="absolute top-2 left-2 text-xs"
|
||||
<div
|
||||
v-if="event.image && !compact"
|
||||
class="relative aspect-[16/9] overflow-hidden rounded-t-lg"
|
||||
>
|
||||
{{ categoryLabel }}
|
||||
</Badge>
|
||||
<img
|
||||
:src="event.image"
|
||||
:alt="event.title"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<!-- Ownership badge — the creator can spot their own events at a
|
||||
<!-- Category badge -->
|
||||
<Badge
|
||||
v-if="categoryLabel"
|
||||
variant="secondary"
|
||||
class="absolute top-2 left-2 text-xs"
|
||||
>
|
||||
{{ categoryLabel }}
|
||||
</Badge>
|
||||
|
||||
<!-- Ownership badge — the creator can spot their own events at a
|
||||
glance on the feed. -->
|
||||
<Badge
|
||||
v-if="event.isMine"
|
||||
variant="outline"
|
||||
class="absolute bottom-2 right-2 text-xs gap-1 bg-background/80 backdrop-blur"
|
||||
>
|
||||
<User class="w-3 h-3" />
|
||||
Yours
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="event.isMine"
|
||||
variant="outline"
|
||||
class="absolute bottom-2 right-2 text-xs gap-1 bg-background/80 backdrop-blur"
|
||||
>
|
||||
<User class="w-3 h-3" />
|
||||
Yours
|
||||
</Badge>
|
||||
|
||||
<!-- Price badge -->
|
||||
<Badge
|
||||
v-if="priceDisplay"
|
||||
class="absolute top-2 right-2 text-xs"
|
||||
>
|
||||
{{ priceDisplay }}
|
||||
</Badge>
|
||||
<!-- Price badge -->
|
||||
<Badge v-if="priceDisplay" class="absolute top-2 right-2 text-xs">
|
||||
{{ priceDisplay }}
|
||||
</Badge>
|
||||
|
||||
<!-- Past badge — shown when the event has already ended. The
|
||||
<!-- Past badge — shown when the event has already ended. The
|
||||
pending/rejected status badge that used to share this slot
|
||||
is now an absolute overlay on Card root, above the wash-out,
|
||||
so we still suppress Past when isNonApproved (the status
|
||||
badge is more actionable in that case). -->
|
||||
<Badge
|
||||
v-if="isPast && !isNonApproved"
|
||||
variant="outline"
|
||||
class="absolute bottom-2 left-2 text-xs gap-1 bg-background/80 backdrop-blur"
|
||||
>
|
||||
<History class="w-3 h-3" />
|
||||
{{ t('events.filters.past', 'Past') }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<CardContent
|
||||
:class="compact ? 'p-3 flex-1 flex flex-col gap-1.5' : 'p-4 flex-1 flex flex-col gap-2'"
|
||||
>
|
||||
<!-- Inline badge row (no-image variant + compact variant). Same
|
||||
badges as the image-overlay set, stacked horizontally at the
|
||||
top of the content area. The "Yours" chip is dropped in
|
||||
compact mode since every card in the hosting view is owned. -->
|
||||
<div v-if="!event.image || compact" class="flex flex-wrap items-center gap-1.5">
|
||||
<Badge v-if="categoryLabel" variant="secondary" class="text-xs">
|
||||
{{ categoryLabel }}
|
||||
</Badge>
|
||||
<Badge v-if="priceDisplay" class="text-xs">
|
||||
{{ priceDisplay }}
|
||||
</Badge>
|
||||
<Badge v-if="event.isMine && !compact" variant="outline" class="text-xs gap-1">
|
||||
<User class="w-3 h-3" />
|
||||
Yours
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="isPast && !isNonApproved"
|
||||
variant="outline"
|
||||
class="text-xs gap-1"
|
||||
class="absolute bottom-2 left-2 text-xs gap-1 bg-background/80 backdrop-blur"
|
||||
>
|
||||
<History class="w-3 h-3" />
|
||||
{{ t('events.filters.past', 'Past') }}
|
||||
{{ t("events.filters.past", "Past") }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Title + Bookmark. Compact mode hides the bookmark (host's
|
||||
<CardContent
|
||||
:class="
|
||||
compact
|
||||
? 'p-3 flex-1 flex flex-col gap-1.5'
|
||||
: 'p-4 flex-1 flex flex-col gap-2'
|
||||
"
|
||||
>
|
||||
<!-- Inline badge row (no-image variant + compact variant). Same
|
||||
badges as the image-overlay set, stacked horizontally at the
|
||||
top of the content area. The "Yours" chip is dropped in
|
||||
compact mode since every card in the hosting view is owned. -->
|
||||
<div
|
||||
v-if="!event.image || compact"
|
||||
class="flex flex-wrap items-center gap-1.5"
|
||||
>
|
||||
<Badge v-if="categoryLabel" variant="secondary" class="text-xs">
|
||||
{{ categoryLabel }}
|
||||
</Badge>
|
||||
<Badge v-if="priceDisplay" class="text-xs">
|
||||
{{ priceDisplay }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="event.isMine && !compact"
|
||||
variant="outline"
|
||||
class="text-xs gap-1"
|
||||
>
|
||||
<User class="w-3 h-3" />
|
||||
Yours
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="isPast && !isNonApproved"
|
||||
variant="outline"
|
||||
class="text-xs gap-1"
|
||||
>
|
||||
<History class="w-3 h-3" />
|
||||
{{ t("events.filters.past", "Past") }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Title + Bookmark. Compact mode hides the bookmark (host's
|
||||
own event — bookmarking it would be noise) and clamps the
|
||||
title to a single line. -->
|
||||
<div class="flex items-start gap-1">
|
||||
<h3
|
||||
:class="[
|
||||
'font-semibold text-foreground leading-tight flex-1',
|
||||
compact ? 'text-sm line-clamp-1' : 'line-clamp-2',
|
||||
]"
|
||||
<div class="flex items-start gap-1">
|
||||
<h3
|
||||
:class="[
|
||||
'font-semibold text-foreground leading-tight flex-1',
|
||||
compact ? 'text-sm line-clamp-1' : 'line-clamp-2',
|
||||
]"
|
||||
>
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
<BookmarkButton
|
||||
v-if="!compact"
|
||||
:pubkey="event.organizer.pubkey"
|
||||
:d-tag="event.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Summary (hidden in compact mode) -->
|
||||
<p
|
||||
v-if="event.summary && !compact"
|
||||
class="text-sm text-muted-foreground line-clamp-2"
|
||||
>
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
<BookmarkButton
|
||||
v-if="!compact"
|
||||
:pubkey="event.organizer.pubkey"
|
||||
:d-tag="event.id"
|
||||
/>
|
||||
</div>
|
||||
{{ event.summary }}
|
||||
</p>
|
||||
|
||||
<!-- Summary (hidden in compact mode) -->
|
||||
<p
|
||||
v-if="event.summary && !compact"
|
||||
class="text-sm text-muted-foreground line-clamp-2"
|
||||
>
|
||||
{{ event.summary }}
|
||||
</p>
|
||||
|
||||
<div :class="compact ? 'space-y-1 text-xs' : 'mt-auto space-y-1.5 pt-2'">
|
||||
<!-- Organizer — small avatar + display name. Hidden in compact
|
||||
<div
|
||||
:class="compact ? 'space-y-1 text-xs' : 'mt-auto space-y-1.5 pt-2'"
|
||||
>
|
||||
<!-- Organizer — small avatar + display name. Hidden in compact
|
||||
mode (host's own roster, no need to tell them whose event
|
||||
it is) and on cards the user already owns. -->
|
||||
<OrganizerCard
|
||||
v-if="!compact"
|
||||
:pubkey="event.organizer.pubkey"
|
||||
compact
|
||||
/>
|
||||
<OrganizerCard
|
||||
v-if="!compact"
|
||||
:pubkey="event.organizer.pubkey"
|
||||
compact
|
||||
/>
|
||||
|
||||
<!-- Date/Time -->
|
||||
<div class="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Calendar class="w-3.5 h-3.5 shrink-0" />
|
||||
<span class="truncate">{{ dateDisplay }}</span>
|
||||
</div>
|
||||
<!-- Date/Time -->
|
||||
<div class="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Calendar class="w-3.5 h-3.5 shrink-0" />
|
||||
<span class="truncate">{{ dateDisplay }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div
|
||||
v-if="event.location"
|
||||
class="flex items-center gap-1.5 text-sm text-muted-foreground"
|
||||
>
|
||||
<MapPin class="w-3.5 h-3.5 shrink-0" />
|
||||
<span class="truncate">{{ event.location }}</span>
|
||||
</div>
|
||||
<!-- Location -->
|
||||
<div
|
||||
v-if="event.location"
|
||||
class="flex items-center gap-1.5 text-sm text-muted-foreground"
|
||||
>
|
||||
<MapPin class="w-3.5 h-3.5 shrink-0" />
|
||||
<span class="truncate">{{ event.location }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Tickets available. `available === undefined` means
|
||||
<!-- Tickets available. `available === undefined` means
|
||||
unlimited capacity (no `tickets_available` tag was
|
||||
published); show the price-only line in that case. -->
|
||||
<div
|
||||
v-if="event.ticketInfo"
|
||||
class="flex items-center gap-1.5 text-sm text-muted-foreground"
|
||||
>
|
||||
<Ticket class="w-3.5 h-3.5 shrink-0" />
|
||||
<span v-if="event.ticketInfo.available === undefined">
|
||||
{{ t('events.detail.unlimitedTickets', 'Unlimited tickets') }}
|
||||
</span>
|
||||
<span v-else-if="event.ticketInfo.available > 0">
|
||||
{{ t('events.detail.ticketsRemainingOfTotal', { count: event.ticketInfo.available, total: event.ticketInfo.total }) }}
|
||||
</span>
|
||||
<span v-else class="text-destructive font-medium">
|
||||
{{ t('events.detail.soldOut') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="event.ticketInfo"
|
||||
class="flex items-center gap-1.5 text-sm text-muted-foreground"
|
||||
>
|
||||
<Ticket class="w-3.5 h-3.5 shrink-0" />
|
||||
<span v-if="event.ticketInfo.available === undefined">
|
||||
{{ t("events.detail.unlimitedTickets", "Unlimited tickets") }}
|
||||
</span>
|
||||
<span v-else-if="event.ticketInfo.available > 0">
|
||||
{{
|
||||
t("events.detail.ticketsAvailable", {
|
||||
count: event.ticketInfo.available,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="text-destructive font-medium">
|
||||
{{ t("events.detail.soldOut") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Owned tickets — shown when the current user holds at
|
||||
<!-- Owned tickets — shown when the current user holds at
|
||||
least one paid ticket for this event. Sits next to
|
||||
the availability line so the buyer can see at a glance
|
||||
whether they've already bought in. -->
|
||||
<div
|
||||
v-if="ownedCount > 0"
|
||||
class="flex items-center gap-1.5 text-sm text-primary"
|
||||
>
|
||||
<CheckCircle2 class="w-3.5 h-3.5 shrink-0" />
|
||||
<span>
|
||||
{{ t('events.detail.ticketsOwned', { count: ownedCount }, ownedCount) }}
|
||||
</span>
|
||||
<div
|
||||
v-if="ownedCount > 0"
|
||||
class="flex items-center gap-1.5 text-sm text-primary"
|
||||
>
|
||||
<CheckCircle2 class="w-3.5 h-3.5 shrink-0" />
|
||||
<span>
|
||||
{{
|
||||
t(
|
||||
"events.detail.ticketsOwned",
|
||||
{ count: ownedCount },
|
||||
ownedCount,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardContent>
|
||||
</div>
|
||||
|
||||
<!-- Status badge — absolutely positioned on Card root so it sits
|
||||
|
|
@ -288,7 +318,7 @@ const isNonApproved = computed(
|
|||
variant="destructive"
|
||||
class="absolute -bottom-1 right-2 z-10 text-xs capitalize shadow"
|
||||
>
|
||||
{{ event.lnbitsStatus === 'rejected' ? 'Rejected' : 'Pending review' }}
|
||||
{{ event.lnbitsStatus === "rejected" ? "Rejected" : "Pending review" }}
|
||||
</Badge>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import ngeohash from 'ngeohash'
|
||||
import type { EventCategory } from './category'
|
||||
import type { CalendarTimeEvent, CalendarDateEvent, TicketTags } from './nip52'
|
||||
import type { TicketedEvent } from './ticket'
|
||||
import ngeohash from "ngeohash";
|
||||
import type { EventCategory } from "./category";
|
||||
import type { CalendarTimeEvent, CalendarDateEvent, TicketTags } from "./nip52";
|
||||
import type { TicketedEvent } from "./ticket";
|
||||
|
||||
/**
|
||||
* Unified view model for displaying events in the UI.
|
||||
|
|
@ -9,43 +9,43 @@ import type { TicketedEvent } from './ticket'
|
|||
*/
|
||||
export interface Event {
|
||||
/** Unique identifier (NIP-52 d-tag) */
|
||||
id: string
|
||||
id: string;
|
||||
/** Nostr event ID */
|
||||
nostrEventId: string
|
||||
nostrEventId: string;
|
||||
/** Whether this is a date-only or time-specific event */
|
||||
type: 'date' | 'time'
|
||||
type: "date" | "time";
|
||||
/** Organizer information */
|
||||
organizer: OrganizerInfo
|
||||
organizer: OrganizerInfo;
|
||||
/** Event title */
|
||||
title: string
|
||||
title: string;
|
||||
/** Brief summary */
|
||||
summary?: string
|
||||
summary?: string;
|
||||
/** Full description (markdown) */
|
||||
description: string
|
||||
description: string;
|
||||
/** Banner/poster image URL */
|
||||
image?: string
|
||||
image?: string;
|
||||
/** Start date/time */
|
||||
startDate: Date
|
||||
startDate: Date;
|
||||
/** End date/time */
|
||||
endDate?: Date
|
||||
endDate?: Date;
|
||||
/** Timezone identifier (IANA) */
|
||||
timezone?: string
|
||||
timezone?: string;
|
||||
/** Human-readable location */
|
||||
location?: string
|
||||
location?: string;
|
||||
/** Geographic coordinates (derived from geohash) */
|
||||
coordinates?: { lat: number; lng: number }
|
||||
coordinates?: { lat: number; lng: number };
|
||||
/** NIP-52 geohash (g tag) */
|
||||
geohash?: string
|
||||
geohash?: string;
|
||||
/** Primary category */
|
||||
category?: EventCategory
|
||||
category?: EventCategory;
|
||||
/** All hashtags/tags */
|
||||
tags: string[]
|
||||
tags: string[];
|
||||
/** Ticket pricing info (if ticketed) */
|
||||
ticketInfo?: EventTicketInfo
|
||||
ticketInfo?: EventTicketInfo;
|
||||
/** Whether this is a private/invite-only event */
|
||||
isPrivate: boolean
|
||||
isPrivate: boolean;
|
||||
/** Nostr event created_at timestamp */
|
||||
createdAt: Date
|
||||
createdAt: Date;
|
||||
/**
|
||||
* LNbits approval status, when the event came from the events
|
||||
* extension rather than a Nostr relay. Undefined for events
|
||||
|
|
@ -53,7 +53,7 @@ export interface Event {
|
|||
* events make it onto relays). Used to render a "Pending review"
|
||||
* badge for the creator's own non-approved drafts.
|
||||
*/
|
||||
lnbitsStatus?: 'approved' | 'proposed' | 'rejected'
|
||||
lnbitsStatus?: "approved" | "proposed" | "rejected";
|
||||
/**
|
||||
* Belongs to the current user. Set by the adapter for own LNbits
|
||||
* drafts and by the events-subscribe callback when the Nostr
|
||||
|
|
@ -61,59 +61,65 @@ export interface Event {
|
|||
* "Yours" badge on the feed so the creator can spot their events
|
||||
* at a glance.
|
||||
*/
|
||||
isMine?: boolean
|
||||
isMine?: boolean;
|
||||
}
|
||||
|
||||
export interface OrganizerInfo {
|
||||
pubkey: string
|
||||
name?: string
|
||||
picture?: string
|
||||
nip05?: string
|
||||
pubkey: string;
|
||||
name?: string;
|
||||
picture?: string;
|
||||
nip05?: string;
|
||||
}
|
||||
|
||||
export interface EventTicketInfo {
|
||||
price: number
|
||||
currency: string
|
||||
price: number;
|
||||
currency: string;
|
||||
/** Remaining capacity. Undefined means unlimited. */
|
||||
available?: number
|
||||
/** Running paid count. */
|
||||
sold: number
|
||||
/** Total capacity (available + sold). Undefined means unlimited. */
|
||||
total?: number
|
||||
available?: number;
|
||||
/** Running paid count across every wave. */
|
||||
sold: number;
|
||||
/** Whether the organizer enabled fiat checkout. */
|
||||
allowFiat: boolean
|
||||
allowFiat: boolean;
|
||||
/** Fiat settle currency when allowFiat is true. */
|
||||
fiatCurrency?: string
|
||||
fiatCurrency?: string;
|
||||
/** Rails the organizer enabled (see TicketTags.paymentMethods). */
|
||||
paymentMethods?: string[]
|
||||
paymentMethods?: string[];
|
||||
}
|
||||
|
||||
function ticketTagsToInfo(ticket: TicketTags | undefined): EventTicketInfo | undefined {
|
||||
if (!ticket) return undefined
|
||||
function ticketTagsToInfo(
|
||||
ticket: TicketTags | undefined,
|
||||
): EventTicketInfo | undefined {
|
||||
if (!ticket) return undefined;
|
||||
return {
|
||||
price: ticket.price,
|
||||
currency: ticket.currency,
|
||||
available: ticket.available,
|
||||
sold: ticket.sold,
|
||||
// Capacity isn't published directly; derive it from remaining + sold.
|
||||
// Undefined `available` means unlimited, so total stays undefined too.
|
||||
total: ticket.available !== undefined ? ticket.available + ticket.sold : undefined,
|
||||
// No total. It used to be derived as `available + sold`, which was
|
||||
// already wrong after any organizer edit re-baselined remaining
|
||||
// (aiolabs/webapp#143) and is now meaningless: since events ext
|
||||
// v1.6.8 `tickets_available` is the ACTIVE WAVE's stock while
|
||||
// `tickets_sold` counts every wave, so the two don't share a
|
||||
// denominator. Nothing publishes a capacity, so nothing can show one.
|
||||
allowFiat: ticket.allowFiat,
|
||||
fiatCurrency: ticket.fiatCurrency,
|
||||
paymentMethods: ticket.paymentMethods,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a CalendarTimeEvent to an Event view model
|
||||
*/
|
||||
export function calendarTimeEventToEvent(event: CalendarTimeEvent, organizer?: Partial<OrganizerInfo>): Event {
|
||||
const category = event.hashtags[0] as EventCategory | undefined
|
||||
export function calendarTimeEventToEvent(
|
||||
event: CalendarTimeEvent,
|
||||
organizer?: Partial<OrganizerInfo>,
|
||||
): Event {
|
||||
const category = event.hashtags[0] as EventCategory | undefined;
|
||||
|
||||
return {
|
||||
id: event.dTag,
|
||||
nostrEventId: event.id,
|
||||
type: 'time',
|
||||
type: "time",
|
||||
organizer: {
|
||||
pubkey: event.pubkey,
|
||||
...organizer,
|
||||
|
|
@ -133,25 +139,28 @@ export function calendarTimeEventToEvent(event: CalendarTimeEvent, organizer?: P
|
|||
ticketInfo: ticketTagsToInfo(event.ticket),
|
||||
isPrivate: false,
|
||||
createdAt: new Date(event.createdAt * 1000),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a CalendarDateEvent to an Event view model
|
||||
*/
|
||||
export function calendarDateEventToEvent(event: CalendarDateEvent, organizer?: Partial<OrganizerInfo>): Event {
|
||||
const category = event.hashtags[0] as EventCategory | undefined
|
||||
export function calendarDateEventToEvent(
|
||||
event: CalendarDateEvent,
|
||||
organizer?: Partial<OrganizerInfo>,
|
||||
): Event {
|
||||
const category = event.hashtags[0] as EventCategory | undefined;
|
||||
|
||||
// Parse ISO date string (YYYY-MM-DD) to Date at midnight UTC
|
||||
const parseIsoDate = (dateStr: string): Date => {
|
||||
const [year, month, day] = dateStr.split('-').map(Number)
|
||||
return new Date(Date.UTC(year, month - 1, day))
|
||||
}
|
||||
const [year, month, day] = dateStr.split("-").map(Number);
|
||||
return new Date(Date.UTC(year, month - 1, day));
|
||||
};
|
||||
|
||||
return {
|
||||
id: event.dTag,
|
||||
nostrEventId: event.id,
|
||||
type: 'date',
|
||||
type: "date",
|
||||
organizer: {
|
||||
pubkey: event.pubkey,
|
||||
...organizer,
|
||||
|
|
@ -170,7 +179,7 @@ export function calendarDateEventToEvent(event: CalendarDateEvent, organizer?: P
|
|||
ticketInfo: ticketTagsToInfo(event.ticket),
|
||||
isPrivate: false,
|
||||
createdAt: new Date(event.createdAt * 1000),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -189,18 +198,18 @@ export function ticketedEventToEvent(
|
|||
event: TicketedEvent,
|
||||
organizer?: Partial<OrganizerInfo>,
|
||||
): Event {
|
||||
const hasTime = event.event_start_date.includes('T')
|
||||
const hasTime = event.event_start_date.includes("T");
|
||||
const startDate = hasTime
|
||||
? new Date(event.event_start_date)
|
||||
: parseDateOnly(event.event_start_date)
|
||||
const endRaw = event.event_end_date
|
||||
: parseDateOnly(event.event_start_date);
|
||||
const endRaw = event.event_end_date;
|
||||
const endDate = endRaw
|
||||
? endRaw.includes('T')
|
||||
? endRaw.includes("T")
|
||||
? new Date(endRaw)
|
||||
: parseDateOnly(endRaw)
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
const category = event.categories?.[0] as EventCategory | undefined
|
||||
const category = event.categories?.[0] as EventCategory | undefined;
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
|
|
@ -208,16 +217,16 @@ export function ticketedEventToEvent(
|
|||
// id as a placeholder. Approved + published versions will overwrite
|
||||
// this with the real Nostr event id.
|
||||
nostrEventId: event.id,
|
||||
type: hasTime ? 'time' : 'date',
|
||||
type: hasTime ? "time" : "date",
|
||||
organizer: {
|
||||
// Pending events have no Nostr pubkey yet. Empty string is fine
|
||||
// — the card layer falls back gracefully and the OrganizerCard
|
||||
// is only shown for approved (Nostr-sourced) events anyway.
|
||||
pubkey: '',
|
||||
pubkey: "",
|
||||
...organizer,
|
||||
},
|
||||
title: event.name,
|
||||
description: event.info ?? '',
|
||||
description: event.info ?? "",
|
||||
image: event.banner ?? undefined,
|
||||
startDate,
|
||||
endDate,
|
||||
|
|
@ -229,24 +238,26 @@ export function ticketedEventToEvent(
|
|||
// FastAPI serialization). new Date() handles both ISO strings and
|
||||
// numeric epoch — same shape used in useEvents sorting.
|
||||
createdAt: new Date(event.time) || new Date(),
|
||||
lnbitsStatus: event.status as Event['lnbitsStatus'],
|
||||
lnbitsStatus: event.status as Event["lnbitsStatus"],
|
||||
// fetchMyEvents only returns the caller's own events, so anything
|
||||
// reaching this adapter is by definition mine.
|
||||
isMine: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function parseDateOnly(dateStr: string): Date {
|
||||
const [year, month, day] = dateStr.split('-').map(Number)
|
||||
return new Date(Date.UTC(year, month - 1, day))
|
||||
const [year, month, day] = dateStr.split("-").map(Number);
|
||||
return new Date(Date.UTC(year, month - 1, day));
|
||||
}
|
||||
|
||||
function decodeGeohash(geohash?: string): { lat: number; lng: number } | undefined {
|
||||
if (!geohash) return undefined
|
||||
function decodeGeohash(
|
||||
geohash?: string,
|
||||
): { lat: number; lng: number } | undefined {
|
||||
if (!geohash) return undefined;
|
||||
try {
|
||||
const { latitude, longitude } = ngeohash.decode(geohash)
|
||||
return { lat: latitude, lng: longitude }
|
||||
const { latitude, longitude } = ngeohash.decode(geohash);
|
||||
return { lat: latitude, lng: longitude };
|
||||
} catch {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -483,9 +483,8 @@ function goToMyTickets() {
|
|||
</span>
|
||||
<span v-else>
|
||||
{{
|
||||
t("events.detail.ticketsRemainingOfTotal", {
|
||||
t("events.detail.ticketsAvailable", {
|
||||
count: event.ticketInfo.available,
|
||||
total: event.ticketInfo.total,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
|
|
@ -567,9 +566,8 @@ function goToMyTickets() {
|
|||
<span v-else>
|
||||
{{ priceLabel }} ·
|
||||
{{
|
||||
t("events.detail.ticketsRemainingOfTotal", {
|
||||
t("events.detail.ticketsAvailable", {
|
||||
count: event.ticketInfo.available,
|
||||
total: event.ticketInfo.total,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue