GitGram — import.php — GitGram
TaskGram / main / v1.00 / import.php10,925 B↓ Raw
<?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>&#10003; 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>&#8659; 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">&#43; 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'; ?>
Ready
GitGram