<?php
declare(strict_types=1);
require_once 'includes/config.php';
require_once 'includes/functions.php';
$user = require_login();
$error = '';
$success = '';
$result = null;
/* ── Handle import submission ─────────────────────────────────── */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf()) {
flash('error', 'Invalid request token.');
redirect('import.php');
}
$action = $_POST['action'] ?? '';
$append = ($_POST['mode'] ?? 'append') === 'append';
$list_id = preg_replace('/[^a-f0-9]/', '', $_POST['list_id'] ?? '');
$new_title = trim($_POST['new_title'] ?? '');
// Determine target list
if ($list_id === 'new') {
if ($new_title === '') {
$error = 'Please enter a title for the new list.';
} else {
$new_list = create_list($user['username'], $new_title, '', false);
$list_id = $new_list['id'];
}
}
if (!$error && $list_id !== '') {
// Validate list belongs to user
$list = get_list($user['username'], $list_id);
if (!$list) {
$error = 'List not found.';
}
}
// Read uploaded file
if (!$error) {
$file = $_FILES['import_file'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
$err_codes = [
UPLOAD_ERR_INI_SIZE => 'File exceeds server upload limit.',
UPLOAD_ERR_FORM_SIZE => 'File exceeds form size limit.',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded.',
UPLOAD_ERR_NO_FILE => 'No file selected.',
UPLOAD_ERR_NO_TMP_DIR => 'No temp directory available.',
UPLOAD_ERR_CANT_WRITE => 'Cannot write file to disk.',
];
$error = $err_codes[$file['error'] ?? UPLOAD_ERR_NO_FILE] ?? 'Upload failed.';
} elseif ($file['size'] > 2 * 1024 * 1024) {
$error = 'File too large (max 2 MB).';
}
}
if (!$error) {
$content = file_get_contents($file['tmp_name']);
$orig_name = strtolower($file['name']);
$ext = pathinfo($orig_name, PATHINFO_EXTENSION);
// Auto-detect by extension, then by content
if ($ext === 'csv' || (str_contains($content, ',') && str_contains($content, "\n"))) {
$parsed = parse_csv_import($content);
$type = 'CSV';
} else {
$parsed = parse_txt_import($content);
$type = 'TXT/MD';
}
if (empty($parsed)) {
$error = 'No tasks found in file. Check the format and try again.';
} else {
$res = apply_import_tasks($user['username'], $list_id, $parsed, $append);
if ($res['error']) {
$error = $res['error'];
} else {
$result = [
'added' => $res['added'],
'type' => $type,
'list' => get_list($user['username'], $list_id),
];
}
}
}
}
/* ── Load user's lists for the selector ───────────────────────── */
$my_lists = get_lists_for_user($user['username']);
$page_title = 'Import Tasks';
require_once 'includes/header.php';
?>
<div class="page-wrap" style="max-width:680px; margin-top:12px; margin-bottom:16px;">
<?php if ($result): ?>
<!-- ── Success panel ──────────────────────────────── -->
<div class="window">
<div class="window-title"><span>✓ Import Complete</span></div>
<div class="window-body">
<p style="margin-bottom:10px;">
<strong><?= $result['added'] ?> task<?= $result['added'] !== 1 ? 's' : '' ?></strong>
imported from <?= h($result['type']) ?> into
<strong><?= h($result['list']['title'] ?? '') ?></strong>.
</p>
<div class="d-flex gap-6">
<a href="<?= h(list_url($result['list']['owner'], $result['list']['id'])) ?>"
class="btn btn-primary">Open List</a>
<a href="import.php" class="btn">Import Another</a>
<a href="dashboard.php" class="btn">Dashboard</a>
</div>
</div>
</div>
<?php else: ?>
<!-- ── Import form ────────────────────────────────── -->
<div class="window">
<div class="window-title"><span>⇓ Import Tasks</span></div>
<div class="window-body">
<?php if ($error): ?>
<div class="flash flash-error" style="margin:0 0 10px 0;"><?= h($error) ?></div>
<?php endif; ?>
<form method="post" action="import.php" enctype="multipart/form-data">
<?= csrf_field() ?>
<input type="hidden" name="action" value="import">
<!-- File picker -->
<div class="form-row">
<label for="import_file">File to Import
<span style="font-weight:normal; font-size:11px;">(.csv, .txt, .md — max 2 MB)</span>
</label>
<input type="file" id="import_file" name="import_file"
accept=".csv,.txt,.md,text/csv,text/plain,text/markdown"
required
style="border:none; background:transparent; padding:2px 0; width:100%;">
</div>
<!-- Target list -->
<div class="form-row">
<label for="list_id">Import Into</label>
<select id="list_id" name="list_id" required>
<option value="">— select a list —</option>
<option value="new">+ Create new list from file</option>
<?php foreach ($my_lists as $lst): ?>
<option value="<?= h($lst['id']) ?>">
<?= h($lst['title']) ?>
(<?= $lst['active_count'] ?> active, <?= $lst['completed_count'] ?> done)
</option>
<?php endforeach; ?>
</select>
</div>
<!-- New list title (shown when "new" selected, but always present in DOM for submit) -->
<div class="form-row" id="new-title-row"
style="border-left:3px solid var(--chrome-dark); padding-left:8px;">
<label for="new_title">New List Title</label>
<input type="text" id="new_title" name="new_title"
maxlength="128" placeholder="Enter title for new list…">
</div>
<!-- Append or replace -->
<div class="form-row">
<label>Import Mode</label>
<div style="display:flex; gap:16px; flex-wrap:wrap; margin-top:4px;">
<label style="font-weight:normal; cursor:pointer;">
<input type="radio" name="mode" value="append" checked>
Append to existing tasks
</label>
<label style="font-weight:normal; cursor:pointer;">
<input type="radio" name="mode" value="replace">
Replace all tasks in list
</label>
</div>
</div>
<div class="sep"></div>
<div class="d-flex gap-6">
<button type="submit" class="btn btn-primary">Import</button>
<a href="dashboard.php" class="btn">Cancel</a>
</div>
</form>
</div>
</div>
<!-- ── Format reference ───────────────────────────── -->
<div class="window mt-10">
<div class="window-title"><span>Supported Formats</span></div>
<div class="window-body" style="font-size:12px;">
<div style="display:flex; gap:12px; flex-wrap:wrap;">
<div style="flex:1; min-width:220px;">
<div class="section-title">CSV</div>
<p style="margin-bottom:6px; color:var(--chrome-darker);">
Header row is auto-detected. Recognised columns:
</p>
<code style="display:block; background:#fff;
border-top:2px solid var(--chrome-dark);
border-left:2px solid var(--chrome-dark);
border-bottom:2px solid var(--chrome-light);
border-right:2px solid var(--chrome-light);
padding:6px 8px; font-size:11px; white-space:pre;">status,priority,order,text,created,completed
active,high,1,Buy groceries,,,
completed,normal,,Pay rent,,2026-01-15
active,low,,Read book,,,</code>
<p style="margin-top:6px; color:var(--chrome-darker);">
Status: <code>active</code> / <code>completed</code><br>
Priority: <code>high</code> / <code>normal</code> / <code>low</code><br>
Single-column files (text only) are also accepted.
</p>
</div>
<div style="flex:1; min-width:220px;">
<div class="section-title">TXT / Markdown</div>
<p style="margin-bottom:6px; color:var(--chrome-darker);">
One task per line. Markdown task-list syntax supported:
</p>
<code style="display:block; background:#fff;
border-top:2px solid var(--chrome-dark);
border-left:2px solid var(--chrome-dark);
border-bottom:2px solid var(--chrome-light);
border-right:2px solid var(--chrome-light);
padding:6px 8px; font-size:11px; white-space:pre;">- [ ] Normal task
- [x] Completed task
- [HIGH] Urgent thing !high
- [LOW] Someday maybe !low
Plain text also works
* Another bullet style</code>
<p style="margin-top:6px; color:var(--chrome-darker);">
Priority prefix: <code>[HIGH]</code> / <code>[LOW]</code><br>
Priority suffix: <code>!high</code> / <code>!low</code><br>
Lines starting with <code>#</code> are skipped.
</p>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php require_once 'includes/footer.php'; ?>