feat(sails): support configuring available sails list in VesselForm and interactive sails selector in LogEntryEditor

This commit is contained in:
2026-05-28 12:44:30 +02:00
parent 19082dcae1
commit e85beba2bc
5 changed files with 275 additions and 5 deletions
+69 -1
View File
@@ -44,12 +44,13 @@ interface LogEvent {
}
export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryEditorProps) {
const { t } = useTranslation()
const { t, i18n } = useTranslation()
const { showAlert } = useDialog()
// General details state
const [date, setDate] = useState('')
const [dayOfTravel, setDayOfTravel] = useState('')
const [yachtSails, setYachtSails] = useState<string[]>([])
const [departure, setDeparture] = useState('')
const [destination, setDestination] = useState('')
@@ -120,6 +121,27 @@ export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryE
setFuelConsumption(cons >= 0 ? String(cons) : '0')
}, [fuelMorning, fuelRefilled, fuelEvening])
// Load Yacht Sails
useEffect(() => {
async function loadYachtSails() {
try {
const masterKey = getActiveMasterKey()
if (!masterKey) return
const yacht = await db.yachts.get(logbookId)
if (yacht) {
const decrypted = await decryptJson(yacht.encryptedData, yacht.iv, yacht.tag, masterKey)
if (decrypted && decrypted.sails && Array.isArray(decrypted.sails)) {
setYachtSails(decrypted.sails)
}
}
} catch (err) {
console.error('Failed to load yacht sails in editor:', err)
}
}
loadYachtSails()
}, [logbookId])
// Load entry details
useEffect(() => {
async function loadEntry() {
@@ -324,6 +346,33 @@ export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryE
}
}
const defaultSails = i18n.language === 'de'
? ['Großsegel', 'Genua', 'Fock', 'Spinnaker', 'Gennaker']
: ['Mainsail', 'Genoa', 'Jib', 'Spinnaker', 'Gennaker']
const toggleSailOrMotor = (item: string) => {
let currentItems = evSailsOrMotor
.split(/\s*(?:\+|\bplus\b|,)\s*/i)
.map(s => s.trim())
.filter(Boolean)
if (currentItems.some(s => s.toLowerCase() === item.toLowerCase())) {
currentItems = currentItems.filter(s => s.toLowerCase() !== item.toLowerCase())
} else {
currentItems.push(item)
}
setEvSailsOrMotor(currentItems.join(' + '))
}
const isItemActive = (item: string) => {
const currentItems = evSailsOrMotor
.split(/\s*(?:\+|\bplus\b|,)\s*/i)
.map(s => s.trim().toLowerCase())
.filter(Boolean)
return currentItems.includes(item.toLowerCase())
}
const handleAddEvent = (e: React.FormEvent) => {
e.preventDefault()
if (!evTime) return
@@ -912,6 +961,25 @@ export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryE
onChange={(e) => setEvSailsOrMotor(e.target.value)}
disabled={saving}
/>
<div className="sails-picker-container">
<div className="sails-picker-pills">
{(yachtSails.length > 0 ? yachtSails : defaultSails).map((sail) => (
<span
key={sail}
className={`sail-pill ${isItemActive(sail) ? 'active' : ''}`}
onClick={() => toggleSailOrMotor(sail)}
>
{sail}
</span>
))}
<span
className={`sail-pill motor-pill ${isItemActive(t('logs.motor_propulsion')) ? 'active' : ''}`}
onClick={() => toggleSailOrMotor(t('logs.motor_propulsion'))}
>
{t('logs.motor_propulsion')}
</span>
</div>
</div>
</div>
<div className="input-group">
+70 -2
View File
@@ -4,7 +4,7 @@ import { db } from '../services/db.js'
import { getActiveMasterKey } from '../services/auth.js'
import { encryptJson, decryptJson } from '../services/crypto.js'
import { syncLogbook } from '../services/sync.js'
import { Ship, Save, Check } from 'lucide-react'
import { Ship, Save, Check, Plus, X } from 'lucide-react'
interface VesselFormProps {
logbookId: string
@@ -20,6 +20,8 @@ export default function VesselForm({ logbookId }: VesselFormProps) {
const [callSign, setCallSign] = useState('')
const [atis, setAtis] = useState('')
const [mmsi, setMmsi] = useState('')
const [sails, setSails] = useState<string[]>([])
const [newSailName, setNewSailName] = useState('')
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
@@ -48,6 +50,7 @@ export default function VesselForm({ logbookId }: VesselFormProps) {
setCallSign(decrypted.callSign || '')
setAtis(decrypted.atis || '')
setMmsi(decrypted.mmsi || '')
setSails(decrypted.sails || [])
}
}
} catch (err: any) {
@@ -61,6 +64,18 @@ export default function VesselForm({ logbookId }: VesselFormProps) {
loadVessel()
}, [logbookId])
const handleAddSail = () => {
const trimmed = newSailName.trim()
if (trimmed && !sails.includes(trimmed)) {
setSails([...sails, trimmed])
}
setNewSailName('')
}
const handleRemoveSail = (indexToRemove: number) => {
setSails(sails.filter((_, idx) => idx !== indexToRemove))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setSaving(true)
@@ -79,7 +94,8 @@ export default function VesselForm({ logbookId }: VesselFormProps) {
registrationNumber: registrationNumber.trim(),
callSign: callSign.trim(),
atis: atis.trim(),
mmsi: mmsi.trim()
mmsi: mmsi.trim(),
sails: sails
}
// E2E encrypt
@@ -226,6 +242,58 @@ export default function VesselForm({ logbookId }: VesselFormProps) {
disabled={saving}
/>
</div>
<div className="sails-section">
<h3>{t('vessel.sails_list')}</h3>
<p className="help-text">{t('vessel.sails_help')}</p>
<div className="sails-badges-grid">
{sails.length === 0 ? (
<span className="no-sails-msg">{t('vessel.no_sails')}</span>
) : (
sails.map((sail, idx) => (
<span key={idx} className="sail-badge">
{sail}
<button
type="button"
className="remove-btn"
onClick={() => handleRemoveSail(idx)}
disabled={saving}
>
<X size={14} />
</button>
</span>
))
)}
</div>
<div className="add-sail-form">
<input
type="text"
className="input-text"
placeholder={t('vessel.sail_name_placeholder')}
value={newSailName}
onChange={(e) => setNewSailName(e.target.value)}
disabled={saving}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddSail();
}
}}
/>
<button
type="button"
className="btn secondary"
onClick={handleAddSail}
disabled={saving || !newSailName.trim()}
style={{ width: 'auto' }}
>
<Plus size={16} />
{t('vessel.add_sail')}
</button>
</div>
</div>
</div>
<div className="form-actions">