mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
5c312a8497
The packing list bar gets an export menu next to the import button, both now icon only. The menu prints the open view as a TREK styled page (trip, dates, packed count, one card per category, bags), which the browser can also save as a PDF, and downloads it as a Markdown checklist or as a CSV in the exact format the import reads, bags included, so a list works as a personal template without admin rights. The import now also reads Markdown checklists: headings become categories, "- [x]" marks an item packed, a "3x" prefix sets the quantity and a "(200 g)" suffix the weight. The phone action sheet offers the same exports through the shared usePackingExport hook. The help center gets an export guide with new pictures, the import guide and the wiki page describe the Markdown format, in all 27 languages. Addresses discussions #1420 and #875.
81 lines
3.3 KiB
TypeScript
81 lines
3.3 KiB
TypeScript
import type { PackingItem } from '../../types'
|
||
|
||
/**
|
||
* A packing list leaving TREK as a file (#875): as Markdown for a notes app, and as
|
||
* CSV in the exact format the import reads. The printable page is its own module,
|
||
* packingPrintDocument.ts, built from the same grouping.
|
||
*
|
||
* Both files follow the order the list shows, so a list that goes out and comes back
|
||
* through the import lands in the same categories, with the same quantities, weights
|
||
* and checkmarks. Markdown carries everything but the bag (bags belong to one trip);
|
||
* CSV carries the bag too.
|
||
*/
|
||
|
||
/** What an export needs to know about a bag: its name, and for the printout its colour. */
|
||
export interface PackingBagInfo {
|
||
name: string
|
||
color?: string | null
|
||
}
|
||
export type PackingBagLookup = (bagId: number) => PackingBagInfo | undefined
|
||
|
||
export interface PackingCategoryGroup {
|
||
category: string
|
||
items: PackingItem[]
|
||
}
|
||
|
||
/** The items under their category, categories in the order their first item appears. */
|
||
export function groupByCategory(items: PackingItem[], uncategorized: string): PackingCategoryGroup[] {
|
||
const groups = new Map<string, PackingItem[]>()
|
||
for (const item of items) {
|
||
const category = item.category?.trim() || uncategorized
|
||
const group = groups.get(category)
|
||
if (group) group.push(item)
|
||
else groups.set(category, [item])
|
||
}
|
||
return [...groups].map(([category, grouped]) => ({ category, items: grouped }))
|
||
}
|
||
|
||
/** "3 × Socks", read back by the import's quantity rule; one is left unsaid. */
|
||
const withQuantity = (item: PackingItem): string =>
|
||
(item.quantity ?? 1) > 1 ? `${item.quantity} × ${item.name}` : item.name
|
||
|
||
/** A file name from the trip title: lowercase, ascii-ish, short. */
|
||
export function packingFileName(tripTitle: string | undefined, extension: string): string {
|
||
const slug = (tripTitle || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40)
|
||
return `packing-list${slug ? `-${slug}` : ''}${extension}`
|
||
}
|
||
|
||
/**
|
||
* A Markdown checklist: the title as the one top heading, a second-level heading per
|
||
* category, and a task-list item per entry with its weight per piece in brackets.
|
||
*/
|
||
export function packingMarkdown(items: PackingItem[], title: string, uncategorized: string): string {
|
||
const lines = [`# ${title}`]
|
||
for (const group of groupByCategory(items, uncategorized)) {
|
||
lines.push('', `## ${group.category}`, '')
|
||
for (const item of group.items) {
|
||
const weight = item.weight_grams ? ` (${item.weight_grams} g)` : ''
|
||
lines.push(`- [${item.checked ? 'x' : ' '}] ${withQuantity(item)}${weight}`)
|
||
}
|
||
}
|
||
return `${lines.join('\n')}\n`
|
||
}
|
||
|
||
/** One CSV field, quoted when it holds a separator or a quote, a quote doubled inside. */
|
||
const csvField = (value: string): string => (/[",;\t\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value)
|
||
|
||
/**
|
||
* The import format, one item per row: Category, Name, Weight, Bag, checked. No header
|
||
* row, because the import would read it as an item.
|
||
*/
|
||
export function packingCsv(items: PackingItem[], bag: PackingBagLookup): string {
|
||
const rows = items.map(item => [
|
||
item.category?.trim() || '',
|
||
withQuantity(item),
|
||
item.weight_grams ? String(item.weight_grams) : '',
|
||
item.bag_id != null ? bag(item.bag_id)?.name ?? '' : '',
|
||
item.checked ? 'checked' : '',
|
||
].map(csvField).join(','))
|
||
return `${rows.join('\n')}\n`
|
||
}
|