✨feature: Add projects homepage and support for adding new projects
This commit is contained in:
+166
-3
@@ -34,6 +34,9 @@ select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
code {
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
}
|
||||
@@ -59,12 +62,17 @@ code {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.workspace-stack {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: var(--accent);
|
||||
@@ -214,10 +222,13 @@ p {
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 11px 18px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
border-color: transparent;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, #70e0c6 100%);
|
||||
@@ -235,6 +246,135 @@ p {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.dashboard-intro,
|
||||
.project-create-panel,
|
||||
.projects-panel {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.dashboard-intro,
|
||||
.project-create-panel,
|
||||
.projects-panel {
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.dashboard-intro {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dashboard-intro-meta {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.project-create-form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.project-create-form label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.project-create-form input,
|
||||
.project-create-form textarea {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.project-create-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
margin-bottom: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.project-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
min-height: 220px;
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(27, 39, 54, 0.94), rgba(14, 21, 31, 0.96)),
|
||||
radial-gradient(circle at top right, rgba(125, 211, 167, 0.14), transparent 38%);
|
||||
box-shadow: var(--shadow);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
border-color 160ms ease,
|
||||
background 160ms ease;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(125, 211, 167, 0.38);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(31, 45, 62, 0.98), rgba(16, 23, 35, 0.98)),
|
||||
radial-gradient(circle at top right, rgba(125, 211, 167, 0.18), transparent 40%);
|
||||
}
|
||||
|
||||
.project-card-body {
|
||||
flex: 1;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.project-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.project-card-slug {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.project-card-cta {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.trash-columns-wrap {
|
||||
margin-top: 18px;
|
||||
}
|
||||
@@ -501,6 +641,19 @@ p {
|
||||
.notes-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-intro {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dashboard-intro-meta {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -529,6 +682,12 @@ p {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-actions,
|
||||
.project-create-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-actions .button {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -537,10 +696,14 @@ p {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.trash-panel .column {
|
||||
.sidebar-actions .button,
|
||||
.project-create-actions .button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.trash-panel .column {
|
||||
width: 100%;
|
||||
}
|
||||
.column {
|
||||
width: min(320px, calc(100vw - 52px));
|
||||
}
|
||||
|
||||
+59
-24
@@ -1,9 +1,67 @@
|
||||
const initialStateNode = document.getElementById('initial-state');
|
||||
const appRoot = document.getElementById('app');
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
const projectCreateForm = document.getElementById('project-create-form');
|
||||
|
||||
const fetchJson = async (url, options = {}) => {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
...(options.headers || {}),
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
|
||||
if (!response.ok || payload.success === false) {
|
||||
throw new Error(payload.error || 'Request failed');
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
const handleError = (error) => {
|
||||
window.alert(error.message || 'Something went wrong.');
|
||||
};
|
||||
|
||||
if (projectCreateForm) {
|
||||
projectCreateForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(projectCreateForm);
|
||||
const submitButton = projectCreateForm.querySelector('button[type="submit"]');
|
||||
|
||||
if (submitButton) {
|
||||
submitButton.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await fetchJson('/api/create-project.php', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: formData.get('title'),
|
||||
slug: formData.get('slug'),
|
||||
body: formData.get('body'),
|
||||
}),
|
||||
});
|
||||
|
||||
window.location.assign(
|
||||
payload.state.projectUrl || `/?project=${encodeURIComponent(payload.state.project.id)}`
|
||||
);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
|
||||
if (submitButton) {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (initialStateNode && appRoot && initialStateNode.textContent !== 'null') {
|
||||
const state = JSON.parse(initialStateNode.textContent);
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
const boardColumnsEl = document.getElementById('board-columns');
|
||||
const trashColumnsEl = document.getElementById('trash-columns');
|
||||
const trashDropzoneEl = document.getElementById('trash-dropzone');
|
||||
@@ -39,25 +97,6 @@ if (initialStateNode && appRoot && initialStateNode.textContent !== 'null') {
|
||||
|
||||
const markdownPreview = (value = '') => escapeHtml(value).replaceAll('\n', '<br>');
|
||||
|
||||
const fetchJson = async (url, options = {}) => {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
...(options.headers || {}),
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
|
||||
if (!response.ok || payload.success === false) {
|
||||
throw new Error(payload.error || 'Request failed');
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
const updateState = (nextState) => {
|
||||
state.project = nextState.project;
|
||||
state.board = nextState.board;
|
||||
@@ -394,10 +433,6 @@ if (initialStateNode && appRoot && initialStateNode.textContent !== 'null') {
|
||||
noteDialog.showModal();
|
||||
};
|
||||
|
||||
const handleError = (error) => {
|
||||
window.alert(error.message || 'Something went wrong.');
|
||||
};
|
||||
|
||||
const reloadBoard = async () => {
|
||||
const response = await fetch(`/api/board-state.php?project=${encodeURIComponent(projectId)}`);
|
||||
updateState(await response.json());
|
||||
|
||||
Reference in New Issue
Block a user