730 lines
26 KiB
JavaScript
730 lines
26 KiB
JavaScript
import { t, i18n } from '../../i18n.js'
|
|
import { router } from '../../router.js'
|
|
import { auth } from '../../services/auth.js'
|
|
import { directus } from '../../services/directus.js'
|
|
import { SUPPORTED_CURRENCIES } from '../../services/currency.js'
|
|
import '../location-picker.js'
|
|
import '../pow-captcha.js'
|
|
|
|
const STORAGE_KEY = 'dgray_create_draft'
|
|
|
|
class PageCreate extends HTMLElement {
|
|
constructor() {
|
|
super()
|
|
this.formData = this.loadDraft() || {
|
|
title: '',
|
|
description: '',
|
|
price: '',
|
|
currency: 'EUR',
|
|
price_mode: 'fiat',
|
|
price_type: 'fixed',
|
|
category: '',
|
|
condition: 'good',
|
|
location: '',
|
|
shipping: false,
|
|
moneroAddress: ''
|
|
}
|
|
this.imageFiles = []
|
|
this.imagePreviews = []
|
|
this.categories = []
|
|
this.submitting = false
|
|
}
|
|
|
|
loadDraft() {
|
|
try {
|
|
const saved = localStorage.getItem(STORAGE_KEY)
|
|
return saved ? JSON.parse(saved) : null
|
|
} catch (e) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
saveDraft() {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.formData))
|
|
} catch (e) {
|
|
// Storage full or unavailable
|
|
}
|
|
}
|
|
|
|
clearDraft() {
|
|
localStorage.removeItem(STORAGE_KEY)
|
|
}
|
|
|
|
async connectedCallback() {
|
|
// Check if logged in
|
|
if (!auth.isLoggedIn()) {
|
|
this.showLoginRequired()
|
|
return
|
|
}
|
|
|
|
this.hasDraft = !!localStorage.getItem(STORAGE_KEY)
|
|
await this.loadCategories()
|
|
this.render()
|
|
this.unsubscribe = i18n.subscribe(() => this.render())
|
|
}
|
|
|
|
showLoginRequired() {
|
|
this.innerHTML = /* html */`
|
|
<div class="create-page">
|
|
<div class="login-required">
|
|
<h2>${t('auth.loginRequired')}</h2>
|
|
<button class="btn btn-primary btn-lg" id="login-btn">${t('auth.login')}</button>
|
|
</div>
|
|
</div>
|
|
`
|
|
this.querySelector('#login-btn')?.addEventListener('click', () => {
|
|
const authModal = document.querySelector('auth-modal')
|
|
if (authModal) {
|
|
authModal.show('login')
|
|
authModal.addEventListener('login', () => {
|
|
this.connectedCallback()
|
|
}, { once: true })
|
|
}
|
|
})
|
|
}
|
|
|
|
async loadCategories() {
|
|
try {
|
|
this.categories = await directus.getCategories()
|
|
} catch (e) {
|
|
console.error('Failed to load categories:', e)
|
|
this.categories = []
|
|
}
|
|
}
|
|
|
|
validateMoneroAddress(address) {
|
|
if (!address) return false
|
|
// Standard addresses: start with 4, 95 chars
|
|
// Subaddresses: start with 8, 95 chars
|
|
// Integrated addresses: start with 4, 106 chars
|
|
const standardRegex = /^4[0-9A-Za-z]{94}$/
|
|
const subaddressRegex = /^8[0-9A-Za-z]{94}$/
|
|
const integratedRegex = /^4[0-9A-Za-z]{105}$/
|
|
return standardRegex.test(address) || subaddressRegex.test(address) || integratedRegex.test(address)
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.unsubscribe) this.unsubscribe()
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = /* html */`
|
|
<div class="create-page">
|
|
<h1 data-i18n="create.title">${t('create.title')}</h1>
|
|
|
|
${this.hasDraft ? `
|
|
<div class="draft-notice">
|
|
<span>${t('create.draftRestored')}</span>
|
|
<button type="button" class="btn-link" id="clear-draft-btn">${t('create.clearDraft')}</button>
|
|
</div>
|
|
` : ''}
|
|
|
|
<form id="create-form" class="create-form">
|
|
<div class="form-group">
|
|
<label class="label" for="title" data-i18n="create.listingTitle">${t('create.listingTitle')}</label>
|
|
<input
|
|
type="text"
|
|
class="input"
|
|
id="title"
|
|
name="title"
|
|
value="${this.escapeHtml(this.formData.title)}"
|
|
required
|
|
data-i18n-placeholder="create.titlePlaceholder"
|
|
placeholder="${t('create.titlePlaceholder')}"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="label" for="category">${t('create.category')}</label>
|
|
<select class="input" id="category" name="category" required>
|
|
<option value="">${t('create.selectCategory')}</option>
|
|
${this.categories.map(cat => `
|
|
<option value="${cat.id}" ${this.formData.category === cat.id ? 'selected' : ''}>
|
|
${t(`categories.${cat.slug}`) || cat.name}
|
|
</option>
|
|
`).join('')}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="label" for="condition">${t('create.condition')}</label>
|
|
<select class="input" id="condition" name="condition" required>
|
|
<option value="new" ${this.formData.condition === 'new' ? 'selected' : ''}>${t('create.conditionNew')}</option>
|
|
<option value="like_new" ${this.formData.condition === 'like_new' ? 'selected' : ''}>${t('create.conditionLikeNew')}</option>
|
|
<option value="good" ${this.formData.condition === 'good' ? 'selected' : ''}>${t('create.conditionGood')}</option>
|
|
<option value="fair" ${this.formData.condition === 'fair' ? 'selected' : ''}>${t('create.conditionFair')}</option>
|
|
<option value="poor" ${this.formData.condition === 'poor' ? 'selected' : ''}>${t('create.conditionPoor')}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group form-group-price">
|
|
<label class="label" for="price">${t('create.price')}</label>
|
|
<input
|
|
type="number"
|
|
class="input"
|
|
id="price"
|
|
name="price"
|
|
value="${this.formData.price}"
|
|
min="0"
|
|
step="0.0001"
|
|
required
|
|
placeholder="0.00"
|
|
>
|
|
</div>
|
|
<div class="form-group form-group-currency">
|
|
<label class="label" for="currency">${t('create.currency')}</label>
|
|
<select class="input" id="currency" name="currency">
|
|
${SUPPORTED_CURRENCIES.map(cur => `
|
|
<option value="${cur}" ${this.formData.currency === cur ? 'selected' : ''}>${cur}</option>
|
|
`).join('')}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="label" for="price_mode">${t('create.priceMode')}</label>
|
|
<select class="input" id="price_mode" name="price_mode">
|
|
<option value="fiat" ${this.formData.price_mode === 'fiat' ? 'selected' : ''}>${t('create.priceModeFiat')}</option>
|
|
<option value="xmr" ${this.formData.price_mode === 'xmr' ? 'selected' : ''}>${t('create.priceModeXmr')}</option>
|
|
</select>
|
|
<p class="field-hint">${t('create.priceModeHint')}</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" id="shipping" name="shipping" ${this.formData.shipping ? 'checked' : ''}>
|
|
<span>${t('create.shippingAvailable')}</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="label" for="location" data-i18n="create.location">${t('create.location')}</label>
|
|
<location-picker
|
|
id="location-picker"
|
|
placeholder="${t('create.locationPlaceholder')}"
|
|
></location-picker>
|
|
<p class="field-hint">${t('create.locationHint') || 'Stadt oder PLZ eingeben'}</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="label" for="description" data-i18n="create.description">${t('create.description')}</label>
|
|
<textarea
|
|
class="input"
|
|
id="description"
|
|
name="description"
|
|
rows="5"
|
|
required
|
|
data-i18n-placeholder="create.descriptionPlaceholder"
|
|
placeholder="${t('create.descriptionPlaceholder')}"
|
|
>${this.escapeHtml(this.formData.description)}</textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="label" data-i18n="create.images">${t('create.images')}</label>
|
|
<div class="image-upload">
|
|
<input type="file" id="images" name="images" accept="image/*" multiple hidden>
|
|
<label for="images" class="upload-area" id="upload-area">
|
|
<span class="upload-icon">📷</span>
|
|
<span data-i18n="create.uploadImages">${t('create.uploadImages')}</span>
|
|
</label>
|
|
<div class="image-previews" id="image-previews">
|
|
${this.renderImagePreviews()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="label" for="moneroAddress">${t('create.moneroAddress')}</label>
|
|
<input
|
|
type="text"
|
|
class="input"
|
|
id="moneroAddress"
|
|
name="moneroAddress"
|
|
value="${this.escapeHtml(this.formData.moneroAddress)}"
|
|
required
|
|
placeholder="${t('create.moneroPlaceholder')}"
|
|
>
|
|
<p class="field-hint">${t('create.moneroHint')}</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<pow-captcha id="pow-captcha"></pow-captcha>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-outline btn-lg" id="cancel-btn">
|
|
${t('create.cancel')}
|
|
</button>
|
|
<button type="submit" class="btn btn-primary btn-lg">
|
|
${t('create.publish')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`
|
|
|
|
this.setupEventListeners()
|
|
}
|
|
|
|
setupEventListeners() {
|
|
const form = this.querySelector('#create-form')
|
|
const cancelBtn = this.querySelector('#cancel-btn')
|
|
const imageInput = this.querySelector('#images')
|
|
|
|
form.addEventListener('submit', (e) => this.handleSubmit(e))
|
|
cancelBtn.addEventListener('click', () => router.back())
|
|
|
|
form.querySelectorAll('input:not([type="checkbox"]), textarea, select').forEach(input => {
|
|
input.addEventListener('input', (e) => {
|
|
if (e.target.name) {
|
|
this.formData[e.target.name] = e.target.value
|
|
this.saveDraft()
|
|
}
|
|
})
|
|
})
|
|
|
|
// Checkbox handler
|
|
const shippingCheckbox = this.querySelector('#shipping')
|
|
shippingCheckbox?.addEventListener('change', (e) => {
|
|
this.formData.shipping = e.target.checked
|
|
this.saveDraft()
|
|
})
|
|
|
|
// Location picker handler
|
|
const locationPicker = this.querySelector('#location-picker')
|
|
locationPicker?.addEventListener('location-select', (e) => {
|
|
this.formData.locationData = e.detail
|
|
this.formData.location = e.detail.name
|
|
this.saveDraft()
|
|
})
|
|
|
|
imageInput?.addEventListener('change', (e) => this.handleImageSelect(e))
|
|
|
|
// Clear draft button
|
|
this.querySelector('#clear-draft-btn')?.addEventListener('click', () => {
|
|
this.clearDraft()
|
|
this.formData = {
|
|
title: '',
|
|
description: '',
|
|
price: '',
|
|
currency: 'EUR',
|
|
price_mode: 'fiat',
|
|
price_type: 'fixed',
|
|
category: '',
|
|
condition: 'good',
|
|
location: '',
|
|
shipping: false,
|
|
moneroAddress: ''
|
|
}
|
|
this.hasDraft = false
|
|
this.render()
|
|
this.setupEventListeners()
|
|
})
|
|
}
|
|
|
|
handleImageSelect(e) {
|
|
const files = Array.from(e.target.files)
|
|
|
|
files.forEach(file => {
|
|
if (this.imageFiles.length >= 5) return
|
|
this.imageFiles.push(file)
|
|
|
|
const reader = new FileReader()
|
|
reader.onload = (e) => {
|
|
this.imagePreviews.push(e.target.result)
|
|
this.updateImagePreviews()
|
|
}
|
|
reader.readAsDataURL(file)
|
|
})
|
|
}
|
|
|
|
updateImagePreviews() {
|
|
const container = this.querySelector('#image-previews')
|
|
const uploadArea = this.querySelector('#upload-area')
|
|
|
|
if (container) {
|
|
container.innerHTML = this.renderImagePreviews()
|
|
this.setupRemoveListeners()
|
|
}
|
|
|
|
if (uploadArea) {
|
|
uploadArea.style.display = this.imageFiles.length >= 5 ? 'none' : 'flex'
|
|
}
|
|
}
|
|
|
|
renderImagePreviews() {
|
|
if (this.imagePreviews.length === 0) return ''
|
|
|
|
return this.imagePreviews.map((src, index) => /* html */`
|
|
<div class="image-preview">
|
|
<img src="${src}" alt="Preview ${index + 1}">
|
|
<button type="button" class="remove-image" data-index="${index}" aria-label="Remove">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
`).join('')
|
|
}
|
|
|
|
setupRemoveListeners() {
|
|
this.querySelectorAll('.remove-image').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const index = parseInt(e.currentTarget.dataset.index)
|
|
this.imageFiles.splice(index, 1)
|
|
this.imagePreviews.splice(index, 1)
|
|
this.updateImagePreviews()
|
|
})
|
|
})
|
|
}
|
|
|
|
async handleSubmit(e) {
|
|
e.preventDefault()
|
|
|
|
if (this.submitting) return
|
|
|
|
// Validate PoW Captcha
|
|
const captcha = this.querySelector('#pow-captcha')
|
|
if (!captcha?.isSolved()) {
|
|
this.showError(t('captcha.error'))
|
|
return
|
|
}
|
|
|
|
// Validate Monero address
|
|
if (this.formData.moneroAddress && !this.validateMoneroAddress(this.formData.moneroAddress)) {
|
|
this.showError(t('create.invalidMoneroAddress'))
|
|
return
|
|
}
|
|
|
|
this.submitting = true
|
|
this.clearError()
|
|
|
|
const form = e.target
|
|
const submitBtn = form.querySelector('[type="submit"]')
|
|
submitBtn.disabled = true
|
|
submitBtn.textContent = t('create.publishing')
|
|
|
|
try {
|
|
// Upload images first
|
|
let imageIds = []
|
|
if (this.imageFiles.length > 0) {
|
|
const uploadedFiles = await directus.uploadMultipleFiles(this.imageFiles)
|
|
imageIds = uploadedFiles.map(f => f.id)
|
|
}
|
|
|
|
// Create listing
|
|
const listingData = {
|
|
title: this.formData.title,
|
|
slug: this.generateSlug(this.formData.title),
|
|
description: this.formData.description,
|
|
price: String(parseFloat(this.formData.price) || 0),
|
|
currency: this.formData.currency,
|
|
status: 'published'
|
|
}
|
|
|
|
// Add optional fields only if set
|
|
if (this.formData.price_mode) listingData.price_mode = this.formData.price_mode
|
|
if (this.formData.category) listingData.category = this.formData.category
|
|
if (this.formData.condition) listingData.condition = this.formData.condition
|
|
if (this.formData.shipping) listingData.shipping = this.formData.shipping
|
|
if (this.formData.moneroAddress) listingData.monero_address = this.formData.moneroAddress
|
|
|
|
// Handle location - find or create in locations collection
|
|
if (this.formData.locationData) {
|
|
const locationId = await this.findOrCreateLocation(this.formData.locationData)
|
|
if (locationId) {
|
|
listingData.location = locationId
|
|
}
|
|
}
|
|
|
|
// Add images in junction table format
|
|
if (imageIds.length > 0) {
|
|
listingData.images = {
|
|
create: imageIds.map((id, index) => ({
|
|
directus_files_id: id,
|
|
sort: index
|
|
}))
|
|
}
|
|
}
|
|
|
|
const listing = await directus.createListing(listingData)
|
|
|
|
this.clearDraft()
|
|
|
|
if (listing?.id) {
|
|
router.navigate(`/listing/${listing.id}`)
|
|
} else {
|
|
// Listing created but no ID returned - go to home
|
|
router.navigate('/')
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to create listing:', error)
|
|
console.error('Error details:', JSON.stringify(error.data, null, 2))
|
|
submitBtn.disabled = false
|
|
submitBtn.textContent = t('create.publish')
|
|
this.submitting = false
|
|
|
|
// Extract detailed error message
|
|
const errorMsg = error.data?.errors?.[0]?.message || error.message || t('create.publishFailed')
|
|
this.showError(errorMsg)
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
let errorDiv = this.querySelector('.form-error')
|
|
if (!errorDiv) {
|
|
errorDiv = document.createElement('div')
|
|
errorDiv.className = 'form-error'
|
|
this.querySelector('.form-actions')?.insertAdjacentElement('beforebegin', errorDiv)
|
|
}
|
|
errorDiv.textContent = message
|
|
}
|
|
|
|
clearError() {
|
|
this.querySelector('.form-error')?.remove()
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div')
|
|
div.textContent = text
|
|
return div.innerHTML
|
|
}
|
|
|
|
generateSlug(title) {
|
|
return title
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-|-$/g, '')
|
|
.substring(0, 100)
|
|
}
|
|
|
|
async findOrCreateLocation(locationData) {
|
|
if (!locationData || !locationData.name) return null
|
|
|
|
try {
|
|
// Build filter based on available data
|
|
const filter = {}
|
|
|
|
if (locationData.postalCode) {
|
|
filter.postal_code = { _eq: locationData.postalCode }
|
|
}
|
|
if (locationData.countryCode) {
|
|
filter.country = { _eq: locationData.countryCode }
|
|
}
|
|
if (locationData.name) {
|
|
filter.name = { _eq: locationData.name }
|
|
}
|
|
|
|
// Only search if we have at least one filter
|
|
if (Object.keys(filter).length > 0) {
|
|
const existing = await directus.get('/items/locations', {
|
|
filter,
|
|
limit: 1
|
|
})
|
|
|
|
if (existing.data && existing.data.length > 0) {
|
|
return existing.data[0].id
|
|
}
|
|
}
|
|
|
|
// Create new location
|
|
const newLocation = await directus.post('/items/locations', {
|
|
name: locationData.name || '',
|
|
postal_code: locationData.postalCode || null,
|
|
region: locationData.region || null,
|
|
country: locationData.countryCode || null
|
|
})
|
|
|
|
return newLocation.data?.id
|
|
} catch (error) {
|
|
console.error('Failed to find/create location:', error)
|
|
return null
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define('page-create', PageCreate)
|
|
|
|
const style = document.createElement('style')
|
|
style.textContent = /* css */`
|
|
page-create .create-page {
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
padding: var(--space-lg) 0;
|
|
}
|
|
|
|
page-create .create-page h1 {
|
|
margin-bottom: var(--space-xl);
|
|
}
|
|
|
|
page-create .create-form .form-group {
|
|
margin-bottom: var(--space-lg);
|
|
}
|
|
|
|
page-create textarea.input {
|
|
resize: vertical;
|
|
min-height: 120px;
|
|
}
|
|
|
|
page-create .image-upload {
|
|
border: 2px dashed var(--color-border);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
}
|
|
|
|
page-create .upload-area {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--space-2xl);
|
|
cursor: pointer;
|
|
transition: background-color var(--transition-fast);
|
|
}
|
|
|
|
page-create .upload-area:hover {
|
|
background-color: var(--color-bg-secondary);
|
|
}
|
|
|
|
page-create .upload-icon {
|
|
font-size: 2rem;
|
|
margin-bottom: var(--space-sm);
|
|
}
|
|
|
|
page-create .image-previews {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
|
gap: var(--space-sm);
|
|
padding: var(--space-sm);
|
|
}
|
|
|
|
page-create .image-previews:empty {
|
|
display: none;
|
|
}
|
|
|
|
page-create .image-preview {
|
|
position: relative;
|
|
aspect-ratio: 1;
|
|
border-radius: var(--radius-md);
|
|
overflow: hidden;
|
|
}
|
|
|
|
page-create .image-preview img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
page-create .remove-image {
|
|
position: absolute;
|
|
top: var(--space-xs);
|
|
right: var(--space-xs);
|
|
width: 24px;
|
|
height: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--color-error);
|
|
color: white;
|
|
border-radius: var(--radius-full);
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition: opacity var(--transition-fast);
|
|
}
|
|
|
|
page-create .image-preview:hover .remove-image {
|
|
opacity: 1;
|
|
}
|
|
|
|
page-create .field-hint {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-text-muted);
|
|
margin-top: var(--space-xs);
|
|
}
|
|
|
|
page-create .form-row {
|
|
display: flex;
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
page-create .form-group-price {
|
|
flex: 2;
|
|
}
|
|
|
|
page-create .form-group-currency {
|
|
flex: 1;
|
|
}
|
|
|
|
page-create .checkbox-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
cursor: pointer;
|
|
}
|
|
|
|
page-create .checkbox-label input {
|
|
width: 18px;
|
|
height: 18px;
|
|
accent-color: var(--color-accent);
|
|
}
|
|
|
|
page-create .login-required {
|
|
text-align: center;
|
|
padding: var(--space-3xl) 0;
|
|
}
|
|
|
|
page-create .login-required h2 {
|
|
margin-bottom: var(--space-lg);
|
|
}
|
|
|
|
page-create .form-actions {
|
|
display: flex;
|
|
gap: var(--space-md);
|
|
justify-content: flex-end;
|
|
margin-top: var(--space-xl);
|
|
}
|
|
|
|
page-create .form-error {
|
|
padding: var(--space-md);
|
|
background: var(--color-bg-tertiary);
|
|
border: 1px solid var(--color-error);
|
|
border-radius: var(--radius-md);
|
|
color: var(--color-text);
|
|
margin-bottom: var(--space-md);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
page-create .draft-notice {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--space-md);
|
|
padding: var(--space-sm) var(--space-md);
|
|
background: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
margin-bottom: var(--space-lg);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
page-create .btn-link {
|
|
background: none;
|
|
border: none;
|
|
color: var(--color-primary);
|
|
font-size: var(--font-size-sm);
|
|
cursor: pointer;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
page-create .btn-link:hover {
|
|
color: var(--color-primary-hover);
|
|
}
|
|
`
|
|
document.head.appendChild(style)
|