diff --git a/assets/app.js b/assets/app.js
index 41640e5..a75c28f 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -5,22 +5,41 @@ import './bootstrap.js';
* This file will be included onto the page via the importmap() Twig function,
* which should already be in your base.html.twig.
*/
-import './styles/fonts.css';
-import './styles/theme.css';
-import './styles/app.css';
-import './styles/layout.css';
-import './styles/button.css';
-import './styles/card.css';
-import './styles/article.css';
-import './styles/og.css';
-import './styles/form.css';
-import './styles/notice.css';
-import './styles/spinner.css';
-import './styles/a2hs.css';
-import './styles/analytics.css';
-import './styles/modal.css';
-import './styles/utilities.css';
-import './styles/landing.css';
+// 01 - Base styles (theme, fonts, typography, reset)
+import './styles/01-base/fonts.css';
+import './styles/01-base/theme.css';
+import './styles/01-base/spacing.css';
+import './styles/01-base/reset.css';
+import './styles/01-base/typography.css';
+
+// 02 - Layout (grid, header, navigation, main content)
+import './styles/02-layout/layout.css';
+import './styles/02-layout/header.css';
+
+// 03 - Components (reusable UI components)
+import './styles/03-components/button.css';
+import './styles/03-components/cards-shared.css';
+import './styles/03-components/card.css';
+import './styles/03-components/form.css';
+import './styles/03-components/article.css';
+import './styles/03-components/modal.css';
+import './styles/03-components/notice.css';
+import './styles/03-components/spinner.css';
+import './styles/03-components/a2hs.css';
+import './styles/03-components/og.css';
+import './styles/03-components/nostr-previews.css';
+import './styles/03-components/picture-event.css';
+import './styles/03-components/search.css';
+import './styles/03-components/image-upload.css';
+
+// 04 - Page-specific styles
+import './styles/04-pages/landing.css';
+import './styles/04-pages/admin.css';
+import './styles/04-pages/analytics.css';
+import './styles/04-pages/author-media.css';
+
+// 05 - Utilities (last for highest specificity)
+import './styles/05-utilities/utilities.css';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
diff --git a/assets/styles/fonts.css b/assets/styles/01-base/fonts.css
similarity index 63%
rename from assets/styles/fonts.css
rename to assets/styles/01-base/fonts.css
index df52e2d..417a3aa 100644
--- a/assets/styles/fonts.css
+++ b/assets/styles/01-base/fonts.css
@@ -4,7 +4,7 @@
font-family: 'EB Garamond';
font-style: normal;
font-weight: 400;
- src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ src: url('../../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* eb-garamond-italic - latin_latin-ext */
@font-face {
@@ -12,7 +12,7 @@
font-family: 'EB Garamond';
font-style: italic;
font-weight: 400;
- src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ src: url('../../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* eb-garamond-500 - latin_latin-ext */
@font-face {
@@ -20,7 +20,7 @@
font-family: 'EB Garamond';
font-style: normal;
font-weight: 500;
- src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ src: url('../../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* eb-garamond-500italic - latin_latin-ext */
@font-face {
@@ -28,7 +28,7 @@
font-family: 'EB Garamond';
font-style: italic;
font-weight: 500;
- src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ src: url('../../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* eb-garamond-600 - latin_latin-ext */
@font-face {
@@ -36,7 +36,7 @@
font-family: 'EB Garamond';
font-style: normal;
font-weight: 600;
- src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ src: url('../../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* eb-garamond-600italic - latin_latin-ext */
@font-face {
@@ -44,7 +44,7 @@
font-family: 'EB Garamond';
font-style: italic;
font-weight: 600;
- src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ src: url('../../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* eb-garamond-700 - latin_latin-ext */
@font-face {
@@ -52,7 +52,7 @@
font-family: 'EB Garamond';
font-style: normal;
font-weight: 700;
- src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ src: url('../../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* eb-garamond-700italic - latin_latin-ext */
@font-face {
@@ -60,7 +60,7 @@
font-family: 'EB Garamond';
font-style: italic;
font-weight: 700;
- src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ src: url('../../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* eb-garamond-800 - latin_latin-ext */
@font-face {
@@ -68,7 +68,7 @@
font-family: 'EB Garamond';
font-style: normal;
font-weight: 800;
- src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ src: url('../../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* eb-garamond-800italic - latin_latin-ext */
@font-face {
@@ -76,5 +76,5 @@
font-family: 'EB Garamond';
font-style: italic;
font-weight: 800;
- src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ src: url('../../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
diff --git a/assets/styles/01-base/reset.css b/assets/styles/01-base/reset.css
new file mode 100644
index 0000000..ca4faa1
--- /dev/null
+++ b/assets/styles/01-base/reset.css
@@ -0,0 +1,13 @@
+/**
+ * Base Reset
+ * Minimal reset and base element styles
+ */
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+}
diff --git a/assets/styles/01-base/spacing.css b/assets/styles/01-base/spacing.css
new file mode 100644
index 0000000..99d8178
--- /dev/null
+++ b/assets/styles/01-base/spacing.css
@@ -0,0 +1,39 @@
+/**
+ * Spacing System
+ * Define consistent spacing variables based on an 8px base unit
+ * This creates a predictable rhythm across the entire application
+ */
+
+:root {
+ /* Base spacing unit: 8px */
+ --spacing-base: 0.5rem; /* 8px */
+
+ /* Spacing scale (multiples of base unit) */
+ --spacing-0: 0;
+ --spacing-1: 0.25rem; /* 4px - micro spacing */
+ --spacing-2: 0.5rem; /* 8px - small spacing */
+ --spacing-3: 1rem; /* 16px - medium spacing */
+ --spacing-4: 1.5rem; /* 24px - large spacing */
+ --spacing-5: 2rem; /* 32px - xl spacing */
+ --spacing-6: 3rem; /* 48px - xxl spacing */
+ --spacing-7: 4rem; /* 64px - huge spacing */
+ --spacing-8: 6rem; /* 96px - massive spacing */
+
+ /* Common use-case aliases */
+ --spacing-xs: var(--spacing-1);
+ --spacing-sm: var(--spacing-2);
+ --spacing-md: var(--spacing-3);
+ --spacing-lg: var(--spacing-4);
+ --spacing-xl: var(--spacing-5);
+ --spacing-2xl: var(--spacing-6);
+ --spacing-3xl: var(--spacing-7);
+ --spacing-4xl: var(--spacing-8);
+
+ /* Component-specific spacing */
+ --button-padding-y: var(--spacing-2);
+ --button-padding-x: var(--spacing-4);
+ --card-padding: var(--spacing-4);
+ --input-padding: var(--spacing-2);
+ --section-spacing: var(--spacing-6);
+}
+
diff --git a/assets/styles/theme.css b/assets/styles/01-base/theme.css
similarity index 100%
rename from assets/styles/theme.css
rename to assets/styles/01-base/theme.css
diff --git a/assets/styles/01-base/typography.css b/assets/styles/01-base/typography.css
new file mode 100644
index 0000000..a101282
--- /dev/null
+++ b/assets/styles/01-base/typography.css
@@ -0,0 +1,145 @@
+/**
+ * Typography Styles
+ * Base typography including headings, paragraphs, links, and text utilities
+ */
+
+body {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ max-width: 100%;
+ background-color: var(--color-bg);
+ color: var(--color-text);
+ font-family: var(--font-family), sans-serif;
+ margin: 0;
+ padding: 0;
+ line-height: 1.6;
+}
+
+/* Headings */
+h1, h2, h3, h4, h5, h6 {
+ font-family: var(--heading-font), serif;
+ font-weight: 600;
+ line-height: 1.1;
+ color: var(--color-primary);
+ margin: 30px 0 10px;
+}
+
+h1 {
+ font-size: 3.2rem;
+ margin-top: 0.25em;
+ font-weight: 300;
+}
+
+h1.brand {
+ font-family: var(--brand-font), serif;
+ color: var(--color-primary);
+ font-size: 3.2rem;
+}
+
+h1.brand a {
+ color: var(--brand-color);
+ text-decoration: none;
+}
+
+h1.brand a:hover {
+ text-decoration: none;
+}
+
+h1:not(.brand) > a:hover {
+ text-decoration: none;
+ font-weight: 500;
+}
+
+h2 {
+ font-size: 2.2rem;
+}
+
+h2.brand {
+ font-family: var(--brand-font), serif;
+ color: var(--color-primary);
+}
+
+h3 {
+ font-size: 2rem;
+}
+
+h4 {
+ font-size: 1.9rem;
+}
+
+h5 {
+ font-size: 1.75rem;
+}
+
+h6 {
+ font-size: 1.5rem;
+}
+
+/* Sidebar heading overrides */
+aside h1 {
+ font-size: 1.2rem;
+}
+
+aside h2 {
+ font-size: 1.1rem;
+}
+
+aside p.lede {
+ font-size: 1rem;
+}
+
+/* Paragraphs and text */
+p {
+ margin: 0 0 15px;
+}
+
+.lede {
+ font-family: var(--main-body-font), serif;
+ font-size: 1.6rem;
+ word-wrap: break-word;
+ font-weight: 300;
+}
+
+strong:not(>h2), .strong {
+ color: var(--color-primary);
+}
+
+/* Links */
+a {
+ color: var(--color-secondary);
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+/* Images and icons */
+img {
+ max-width: 100%;
+ height: auto;
+}
+
+svg.icon {
+ width: 2em;
+ height: 2em;
+}
+
+/* Utility classes */
+.hidden {
+ display: none;
+}
+
+.divider {
+ border: 2px solid var(--color-primary);
+ margin: 20px 0;
+}
+
+.truncate {
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/assets/styles/02-layout/header.css b/assets/styles/02-layout/header.css
new file mode 100644
index 0000000..18dabd2
--- /dev/null
+++ b/assets/styles/02-layout/header.css
@@ -0,0 +1,175 @@
+/**
+ * Header Component
+ * Main site header with navigation categories
+ */
+
+.header {
+ text-align: center;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ background-color: var(--color-bg);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.header .container {
+ display: flex;
+ flex-direction: column;
+}
+
+.header__categories ul {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 20px;
+ padding: 0;
+}
+
+.header__categories li {
+ list-style: none;
+}
+
+.header__categories li a:hover {
+ text-decoration: none;
+}
+/**
+ * Shared Card Styles
+ * Styles that are used across different card types in the app
+ */
+
+.card {
+ background-color: var(--color-bg);
+ color: var(--color-text);
+ padding: 0;
+ margin: 0 0 2rem 0;
+ border-radius: 0; /* Sharp edges */
+}
+
+.card a:hover {
+ text-decoration: none;
+ color: var(--color-text);
+ cursor: pointer;
+}
+
+.card a:hover h2 {
+ color: var(--color-text);
+}
+
+.card.bordered {
+ border: 2px solid var(--color-border);
+}
+
+.card-header {
+ margin: 10px 0;
+}
+
+.header__image {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+}
+
+.header__image::before {
+ content: "";
+ display: block;
+ padding-top: 56.25%; /* 16:9 aspect ratio */
+}
+
+.header__image img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.card-body {
+ font-size: 1rem;
+}
+
+.card-footer {
+ border-top: 1px solid var(--color-border);
+ margin: 20px 0;
+}
+
+/* Featured cards layout */
+.featured-cat {
+ border-bottom: 2px solid var(--color-border);
+ padding-left: 10px;
+}
+
+.featured-list {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+
+.featured-list > * {
+ box-sizing: border-box;
+ margin-bottom: 10px;
+ padding: 10px;
+}
+
+div:nth-child(odd) .featured-list {
+ flex-direction: row-reverse;
+}
+
+.featured-list div:first-child {
+ flex: 0 0 66%;
+}
+
+.featured-list div:last-child {
+ flex: 0 0 34%;
+}
+
+.featured-list h2.card-title {
+ font-size: 1.5rem;
+}
+
+.featured-list p.lede {
+ font-size: 1.4rem;
+}
+
+.featured-list .card {
+ margin-bottom: 20px;
+}
+
+.featured-list .card:not(:last-child) {
+ border-bottom: 1px solid var(--color-border);
+}
+
+/* Article list cards */
+.article-list .metadata {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: baseline;
+}
+
+.article-list .metadata p {
+ margin: 0;
+}
+
+/* Responsive adjustments */
+@media (max-width: 1024px) {
+ .featured-list {
+ flex-direction: column !important;
+ }
+
+ .featured-list .card-header {
+ margin-top: 20px;
+ }
+
+ .featured-list .card {
+ border-bottom: 1px solid var(--color-border) !important;
+ }
+
+ .featured-list > * {
+ margin-bottom: 10px;
+ padding: 0;
+ }
+}
+
diff --git a/assets/styles/layout.css b/assets/styles/02-layout/layout.css
similarity index 90%
rename from assets/styles/layout.css
rename to assets/styles/02-layout/layout.css
index b054728..ebe51db 100644
--- a/assets/styles/layout.css
+++ b/assets/styles/02-layout/layout.css
@@ -29,7 +29,7 @@ nav, aside {
}
nav {
- padding: 1em;
+ padding: var(--spacing-3);
overflow-y: auto; /* Ensure the menu is scrollable if content is too long */
}
@@ -47,7 +47,7 @@ nav ul {
}
nav li {
- margin: 0.5em 0;
+ margin: var(--spacing-2) 0;
}
nav a {
@@ -77,7 +77,7 @@ main {
flex-direction: column;
margin-top: 90px;
flex-grow: 1;
- padding: 0 1em;
+ padding: 0 var(--spacing-3);
word-break: break-word;
}
@@ -94,7 +94,7 @@ main.static {
width: 100%;
display: flex;
flex-direction: column;
- gap: 1rem;
+ gap: var(--spacing-3);
}
.user-menu {
@@ -240,17 +240,29 @@ footer {
background-color: #333;
color: white;
text-align: center;
- padding: 1em 0;
+ padding: var(--spacing-3) 0;
position: relative;
width: 100%;
}
footer .footer-links {
- margin: 24px 0;
+ margin: var(--spacing-4) 0;
}
-.search input {
- flex-grow: 1;
+footer a {
+ color: var(--color-accent, #8FCB7E);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+footer a:hover {
+ color: white;
+ text-decoration: underline;
+}
+
+footer p {
+ margin: var(--spacing-2) 0;
+ color: rgba(255, 255, 255, 0.8);
}
nav > header, aside > header { /* prevent global header fixed rules applying to nested headers */
diff --git a/assets/styles/a2hs.css b/assets/styles/03-components/a2hs.css
similarity index 100%
rename from assets/styles/a2hs.css
rename to assets/styles/03-components/a2hs.css
diff --git a/assets/styles/article.css b/assets/styles/03-components/article.css
similarity index 58%
rename from assets/styles/article.css
rename to assets/styles/03-components/article.css
index 7f70aae..6216a65 100644
--- a/assets/styles/article.css
+++ b/assets/styles/03-components/article.css
@@ -1,17 +1,22 @@
+/**
+ * Article Component
+ * Article-specific styling (content, actions, metadata)
+ */
+
.article-main {
- margin-top: 30px;
+ margin-top: var(--spacing-5);
}
.article-main h2, .article-main h3,
.article-main h4, .article-main h5, .article-main h6 {
- margin-top: 2em;
+ margin-top: var(--spacing-5);
}
.article-actions {
display: flex;
justify-content: flex-start;
- gap: 1rem;
- margin: 1rem 0;
+ gap: var(--spacing-3);
+ margin: var(--spacing-3) 0;
}
.article-main p,
@@ -38,28 +43,28 @@
display: flex;
justify-content: space-between;
align-items: baseline;
- margin: 2rem 0;
- padding-top: 0.5rem;
+ margin: var(--spacing-5) 0;
+ padding-top: var(--spacing-2);
border-top: 1px solid var(--color-border);
font-size: 1rem;
}
blockquote {
border-left: 6px solid var(--color-bg-light);
- padding-left: 3px;
- margin: 50px 0 50px 3px;
+ padding-left: var(--spacing-1);
+ margin: var(--spacing-6) 0 var(--spacing-6) var(--spacing-1);
}
blockquote p {
font-size: 1.6rem;
font-style: italic;
color: var(--color-text-mid);
- padding-left: 30px;
+ padding-left: var(--spacing-5);
}
.table-of-contents {
border-left: var(--color-secondary) 6px solid;
- margin: 2em 0;
+ margin: var(--spacing-5) 0;
}
.table-of-contents li {
@@ -70,7 +75,7 @@ blockquote p {
.heading-permalink {
float: left;
padding-right: 0;
- margin-left: -30px;
+ margin-left: calc(var(--spacing-5) * -1);
line-height: 1.2;
color: var(--color-secondary);
}
@@ -111,3 +116,40 @@ blockquote p {
.ql-snow .ql-tooltip.ql-image-tooltip::before {
content: 'Image:';
}
+
+/* Article tags/topics */
+.tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing-2);
+ margin: var(--spacing-4) 0;
+}
+
+.tag {
+ display: inline-block;
+ padding: var(--spacing-1) var(--spacing-3);
+ background-color: var(--color-primary);
+ color: var(--color-text-contrast);
+ border-radius: 1.5rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ text-decoration: none;
+ transition: background-color 0.2s ease, transform 0.2s ease;
+}
+
+.tag:hover {
+ background-color: var(--color-secondary);
+ transform: translateY(-1px);
+ text-decoration: none;
+}
+
+/* Hashtag styling (for inline hashtags in content) */
+.hashtag {
+ color: var(--color-secondary);
+ font-weight: 500;
+}
+
+.hashtag:hover {
+ color: var(--color-primary);
+ text-decoration: underline;
+}
diff --git a/assets/styles/button.css b/assets/styles/03-components/button.css
similarity index 88%
rename from assets/styles/button.css
rename to assets/styles/03-components/button.css
index b2141bd..722c713 100644
--- a/assets/styles/button.css
+++ b/assets/styles/03-components/button.css
@@ -1,9 +1,13 @@
+/**
+ * Button Component
+ * Primary button styles and variants
+ */
button, .btn, a.btn {
background-color: var(--color-primary);
color: var(--color-text-contrast);
border: 2px solid var(--color-primary);
- padding: 10px 20px;
+ padding: var(--button-padding-y) var(--button-padding-x);
text-transform: uppercase;
font-weight: bold;
cursor: pointer;
diff --git a/assets/styles/card.css b/assets/styles/03-components/card.css
similarity index 66%
rename from assets/styles/card.css
rename to assets/styles/03-components/card.css
index 18b9b27..218babd 100644
--- a/assets/styles/card.css
+++ b/assets/styles/03-components/card.css
@@ -1,11 +1,17 @@
+/**
+ * Card Component
+ * Specific card implementations (price lists, comments, etc.)
+ * For shared card styles, see cards-shared.css
+ */
+
h2.card-title {
- margin-top: 10px;
+ margin-top: var(--spacing-2);
}
.price-list {
display: flex;
flex-direction: row;
- gap: 20px;
+ gap: var(--spacing-4);
}
.price-list .card {
@@ -14,17 +20,24 @@ h2.card-title {
justify-content: start;
border: var(--color-primary) solid 1px;
flex-grow: 1;
- padding: 20px;
+ padding: var(--spacing-4);
flex-basis: 300px;
}
.price-list .card .features {
- list-style: none; padding: 0;
+ list-style: none;
+ padding: 0;
}
-.price { font-size: 22px; font-weight: bold; color: var(--color-secondary); }
+.price {
+ font-size: 22px;
+ font-weight: bold;
+ color: var(--color-secondary);
+}
-.price-list .card .features li { padding: 8px 0; }
+.price-list .card .features li {
+ padding: var(--spacing-2) 0;
+}
.price-list .card button:last-child {
margin-top: auto;
@@ -51,7 +64,7 @@ h2.card-title {
display: flex;
flex-direction: column;
background-color: var(--color-bg-light);
- padding: 10px;
+ padding: var(--spacing-2);
}
.card.comment.zap-comment {
@@ -62,7 +75,7 @@ h2.card-title {
display: flex;
align-items: center;
justify-content: space-between;
- margin-bottom: 10px;
+ margin-bottom: var(--spacing-2);
}
.card.comment .metadata p {
diff --git a/assets/styles/03-components/cards-shared.css b/assets/styles/03-components/cards-shared.css
new file mode 100644
index 0000000..e69de29
diff --git a/assets/styles/form.css b/assets/styles/03-components/form.css
similarity index 84%
rename from assets/styles/form.css
rename to assets/styles/03-components/form.css
index 163b3fc..7486941 100644
--- a/assets/styles/form.css
+++ b/assets/styles/03-components/form.css
@@ -1,14 +1,19 @@
+/**
+ * Form Component
+ * Form input styles and layout
+ */
+
form {
display: flex;
flex-direction: column;
clear: both;
- margin-bottom: 1em;
+ margin-bottom: var(--spacing-3);
}
form > div:not(.actions) {
display: flex;
flex-direction: column;
- margin-bottom: 1.5em;
+ margin-bottom: var(--spacing-4);
}
label {
@@ -30,7 +35,7 @@ input, textarea, select, .quill {
}
input, textarea, select {
- padding: 10px;
+ padding: var(--input-padding);
}
textarea, input, select {
@@ -65,7 +70,7 @@ input:focus, textarea:focus, select:focus {
.image-with-preview img.avatar {
width: 40px;
height: 40px;
- margin-right: 1em;
+ margin-right: var(--spacing-3);
}
.image-with-preview input {
@@ -86,7 +91,7 @@ fieldset {
}
.actions {
- margin-bottom: 1.5em;
+ margin-bottom: var(--spacing-4);
}
form ul.list-unstyled li > div > label {
diff --git a/assets/styles/03-components/image-upload.css b/assets/styles/03-components/image-upload.css
new file mode 100644
index 0000000..8ee2d93
--- /dev/null
+++ b/assets/styles/03-components/image-upload.css
@@ -0,0 +1,98 @@
+/* Image Upload Dialog Styles */
+.iu-dialog {
+ display: none;
+}
+
+.iu-dialog.active {
+ display: block;
+}
+
+.iu-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 1000;
+}
+
+.iu-modal {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: var(--color-bg, #fff);
+ border-radius: 0.5rem;
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.3);
+ max-width: 90%;
+ width: 500px;
+ max-height: 90vh;
+ overflow-y: auto;
+ z-index: 1001;
+}
+
+.iu-modal .modal-header {
+ padding: var(--spacing-3);
+ border-bottom: 1px solid var(--color-border, #dee2e6);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.iu-modal .modal-header h5 {
+ margin: 0;
+}
+
+.iu-modal .modal-header .close {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ color: var(--color-text);
+}
+
+.iu-modal .modal-body {
+ padding: var(--spacing-3);
+}
+
+.iu-modal .modal-body > div {
+ margin-bottom: var(--spacing-3);
+}
+
+.upload-area {
+ border: 2px dashed #ccc;
+ padding: var(--spacing-5);
+ text-align: center;
+ cursor: pointer;
+ min-height: 4em;
+ border-radius: 0.25rem;
+ transition: border-color 0.2s;
+}
+
+.upload-area:hover {
+ border-color: var(--color-primary, #007bff);
+}
+
+.upload-area input[type="file"] {
+ display: none;
+}
+
+.upload-progress {
+ display: none;
+ margin-top: 1em;
+}
+
+.upload-progress.active {
+ display: block;
+}
+
+.upload-error {
+ color: red;
+ margin-top: 1em;
+ display: none;
+}
+
+.upload-error.active {
+ display: block;
+}
diff --git a/assets/styles/modal.css b/assets/styles/03-components/modal.css
similarity index 83%
rename from assets/styles/modal.css
rename to assets/styles/03-components/modal.css
index 758f5e2..2b630d1 100644
--- a/assets/styles/modal.css
+++ b/assets/styles/03-components/modal.css
@@ -4,6 +4,7 @@
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
+
.iu-modal {
position: fixed;
top: 50%;
@@ -13,16 +14,18 @@
width: min(600px, 90vw);
max-height: 85vh;
overflow: auto;
- padding: 1rem 1.25rem;
+ padding: var(--spacing-3) var(--spacing-4);
z-index: 1000;
}
+
.iu-modal .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
- gap: .5rem;
- margin-bottom: .5rem;
+ gap: var(--spacing-2);
+ margin-bottom: var(--spacing-2);
}
+
.iu-modal .close {
appearance: none;
border: 0;
diff --git a/assets/styles/03-components/nostr-previews.css b/assets/styles/03-components/nostr-previews.css
new file mode 100644
index 0000000..22605bc
--- /dev/null
+++ b/assets/styles/03-components/nostr-previews.css
@@ -0,0 +1,56 @@
+/**
+ * Nostr Previews Component
+ * Styles for Nostr event and profile preview cards
+ * Converted from SCSS to plain CSS
+ */
+
+.nostr-preview {
+ margin-top: var(--spacing-2);
+}
+
+.nostr-preview .nostr-event-preview,
+.nostr-preview .nostr-profile-preview {
+ border-left: 3px solid #6c5ce7;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.nostr-preview .nostr-profile-preview {
+ border-left-color: #00b894;
+}
+
+.nostr-preview .card-title {
+ margin-bottom: var(--spacing-2);
+ font-size: 1rem;
+}
+
+.nostr-preview .card-text {
+ font-size: 0.9rem;
+}
+
+.nostr-preview .card-footer {
+ padding: var(--spacing-2) var(--spacing-3);
+}
+
+.nostr-previews h6 {
+ font-size: 0.9rem;
+ margin-bottom: var(--spacing-3);
+}
+
+.nostr-previews .preview-container {
+ max-height: 500px;
+ overflow-y: auto;
+ padding-right: var(--spacing-2);
+}
+
+/* Style for nostr links in text */
+.nostr-link {
+ color: #6c5ce7;
+ background-color: rgba(108, 92, 231, 0.1);
+ padding: 0 var(--spacing-1);
+ border-radius: 3px;
+ text-decoration: none;
+}
+
+.nostr-link:hover {
+ background-color: rgba(108, 92, 231, 0.2);
+}
diff --git a/assets/styles/notice.css b/assets/styles/03-components/notice.css
similarity index 69%
rename from assets/styles/notice.css
rename to assets/styles/03-components/notice.css
index 7ecc1e3..912ac59 100644
--- a/assets/styles/notice.css
+++ b/assets/styles/03-components/notice.css
@@ -1,7 +1,7 @@
.notice {
- padding: 10px; /* Padding around the content */
+ padding: var(--spacing-2); /* Padding around the content */
border-radius: 5px; /* Rounded corners */
- margin: 10px 0; /* Margin above and below the notice */
+ margin: var(--spacing-2) 0; /* Margin above and below the notice */
}
.notice p {
diff --git a/assets/styles/og.css b/assets/styles/03-components/og.css
similarity index 68%
rename from assets/styles/og.css
rename to assets/styles/03-components/og.css
index d5985ef..b5ad2f2 100644
--- a/assets/styles/og.css
+++ b/assets/styles/03-components/og.css
@@ -1,7 +1,7 @@
.og-preview-card {
max-width: 100%;
- padding: 20px;
- margin: 10px 0;
+ padding: var(--spacing-4);
+ margin: var(--spacing-2) 0;
background-color: var(--color-bg);
}
diff --git a/assets/styles/03-components/picture-event.css b/assets/styles/03-components/picture-event.css
new file mode 100644
index 0000000..e69de29
diff --git a/assets/styles/03-components/search.css b/assets/styles/03-components/search.css
new file mode 100644
index 0000000..d3c594f
--- /dev/null
+++ b/assets/styles/03-components/search.css
@@ -0,0 +1,21 @@
+/* Search Component Styles */
+.search-component {
+ margin-bottom: 1rem;
+}
+
+.search-credits {
+ text-align: right;
+}
+
+.search-credits .help-text {
+ font-size: 0.875rem;
+}
+
+.search-loading {
+ text-align: center;
+}
+
+.search form {
+ margin-bottom: 1rem;
+}
+
diff --git a/assets/styles/spinner.css b/assets/styles/03-components/spinner.css
similarity index 75%
rename from assets/styles/spinner.css
rename to assets/styles/03-components/spinner.css
index a92ccdd..ebea506 100644
--- a/assets/styles/spinner.css
+++ b/assets/styles/03-components/spinner.css
@@ -1,8 +1,14 @@
+/**
+ * Spinner Component
+ * Main loading spinner (dual-ring design)
+ * Use this for full-page or section loading states
+ */
+
.spinner {
display: flex;
justify-content: center;
align-items: center;
- margin: 1em 0;
+ margin: var(--spacing-3) 0;
}
.lds-dual-ring {
@@ -10,12 +16,13 @@
width: 30px;
height: 30px;
}
+
.lds-dual-ring:after {
content: " ";
display: block;
width: 32px;
height: 32px;
- margin: 4px;
+ margin: var(--spacing-1);
border-radius: 50%;
border: 4px solid var(--color-primary);
border-color: var(--color-primary) transparent var(--color-primary) transparent;
diff --git a/assets/styles/04-pages/admin.css b/assets/styles/04-pages/admin.css
new file mode 100644
index 0000000..cb678d3
--- /dev/null
+++ b/assets/styles/04-pages/admin.css
@@ -0,0 +1,193 @@
+/* Admin Panel Styles */
+
+/* Analytics */
+.analytics-container {
+ padding: var(--spacing-3);
+}
+
+.analytics-card {
+ margin-bottom: var(--spacing-5);
+ padding: var(--spacing-3);
+ background-color: var(--color-card-bg, #f8f9fa);
+ border-radius: 0.5rem;
+}
+
+.analytics-stats {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.analytics-stats li {
+ padding: var(--spacing-2) 0;
+}
+
+.analytics-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.analytics-table th {
+ padding: var(--spacing-3);
+ text-align: left;
+ border-bottom: 2px solid var(--color-border, #dee2e6);
+}
+
+.analytics-table th.text-right,
+.analytics-table th[style*="text-align: right"] {
+ min-width: 100px;
+ text-align: right;
+}
+
+.analytics-table td {
+ padding: var(--spacing-3);
+ border-bottom: 1px solid var(--color-border, #dee2e6);
+}
+
+.analytics-table td.text-right {
+ text-align: right;
+}
+
+.analytics-info {
+ padding: var(--spacing-3);
+ background-color: var(--color-info-bg, #e9f5ff);
+ border-radius: 0.25rem;
+}
+
+/* Articles Table */
+.admin-articles-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.admin-articles-table tr {
+ padding-bottom: var(--spacing-1);
+}
+
+.admin-articles-table td {
+ padding: var(--spacing-2);
+ vertical-align: top;
+}
+
+.admin-articles-table button {
+ margin-left: var(--spacing-2);
+}
+
+.admin-articles-table form {
+ display: inline;
+}
+
+/* Magazine Editor */
+.magazine-editor-layout {
+ display: flex;
+ gap: var(--spacing-4);
+}
+
+.magazine-editor-section {
+ flex: 1;
+ min-width: 320px;
+}
+
+.magazine-search-form {
+ margin-bottom: var(--spacing-3);
+ display: flex;
+ gap: var(--spacing-2);
+}
+
+.magazine-search-form input[type="text"] {
+ flex: 1;
+}
+
+.magazine-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.magazine-table th {
+ padding: 0.25rem 0.5rem;
+ text-align: left;
+ border-bottom: 1px solid var(--color-border, #dee2e6);
+}
+
+.magazine-table td {
+ padding: 0.25rem 0.5rem;
+ vertical-align: top;
+}
+
+.magazine-table td.actions {
+ text-align: right;
+}
+
+.magazine-table td.author {
+ white-space: nowrap;
+}
+/* NIP-68 Picture Event Styles */
+.picture-event {
+ margin: 1rem 0;
+}
+
+.picture-title {
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.content-warning {
+ padding: 1rem;
+ background-color: var(--color-warning-bg, #fff3cd);
+ border: 1px solid var(--color-warning-border, #ffc107);
+ border-radius: 0.25rem;
+ margin-bottom: 1rem;
+}
+
+.btn-show-nsfw {
+ margin-left: 0.5rem;
+ padding: 0.25rem 0.5rem;
+ background-color: var(--color-primary);
+ color: white;
+ border: none;
+ border-radius: 0.25rem;
+ cursor: pointer;
+}
+
+.btn-show-nsfw:hover {
+ opacity: 0.9;
+}
+
+.picture-gallery {
+ display: block;
+}
+
+.picture-gallery.hidden {
+ display: none;
+}
+
+.picture-item {
+ position: relative;
+ margin-bottom: 1rem;
+}
+
+.picture-image {
+ max-width: 100%;
+ height: auto;
+ display: block;
+}
+
+.annotated-users {
+ position: relative;
+}
+
+.user-tag {
+ position: absolute;
+ background-color: rgba(0, 0, 0, 0.7);
+ color: white;
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.25rem;
+ font-size: 0.875rem;
+ white-space: nowrap;
+}
+
+.picture-alt {
+ margin-top: 0.5rem;
+ font-style: italic;
+ color: var(--color-text-muted, #6c757d);
+}
diff --git a/assets/styles/analytics.css b/assets/styles/04-pages/analytics.css
similarity index 80%
rename from assets/styles/analytics.css
rename to assets/styles/04-pages/analytics.css
index cd5586f..4464e00 100644
--- a/assets/styles/analytics.css
+++ b/assets/styles/04-pages/analytics.css
@@ -1,14 +1,14 @@
.analytics-container {
max-width: 800px;
margin: 0 auto;
- padding: 20px;
+ padding: var(--spacing-4);
}
.analytics-card {
background: lightgray;
border-radius: 8px;
- padding: 20px;
- margin-bottom: 20px;
+ padding: var(--spacing-4);
+ margin-bottom: var(--spacing-4);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
@@ -18,7 +18,7 @@
}
.analytics-table th, .analytics-table td {
- padding: 10px;
+ padding: var(--spacing-2);
border-bottom: 1px solid var(--color-border);
}
diff --git a/assets/styles/04-pages/author-media.css b/assets/styles/04-pages/author-media.css
new file mode 100644
index 0000000..293e5c2
--- /dev/null
+++ b/assets/styles/04-pages/author-media.css
@@ -0,0 +1,123 @@
+.profile-tabs {
+ display: flex;
+ gap: 1rem;
+ margin: 1.5rem 0;
+ border-bottom: 2px solid #e0e0e0;
+}
+
+.tab-link {
+ padding: 0.75rem 1.5rem;
+ text-decoration: none;
+ color: #666;
+ border-bottom: 3px solid transparent;
+ margin-bottom: -2px;
+ transition: all 0.3s ease;
+}
+
+.tab-link:hover {
+ color: #333;
+ border-bottom-color: #ccc;
+}
+
+.tab-link.active {
+ color: #0066cc;
+ border-bottom-color: #0066cc;
+ font-weight: 600;
+}
+
+.masonry-grid {
+ column-count: 3;
+ column-gap: 1.5rem;
+ margin: 2rem 0;
+}
+
+@media (max-width: 1200px) {
+ .masonry-grid {
+ column-count: 2;
+ }
+}
+
+@media (max-width: 768px) {
+ .masonry-grid {
+ column-count: 1;
+ }
+}
+
+.masonry-item {
+ break-inside: avoid;
+ margin-bottom: 1.5rem;
+ background: #fff;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.masonry-item:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+}
+
+.masonry-link, .masonry-link:hover {
+ display: block;
+ text-decoration: none;
+ color: inherit;
+}
+
+.masonry-image-container {
+ width: 100%;
+ overflow: hidden;
+ background-color: #f5f5f5;
+}
+
+.masonry-image {
+ width: 100%;
+ height: auto;
+ display: block;
+ transition: transform 0.3s ease;
+}
+
+.masonry-item:hover .masonry-image {
+ transform: scale(1.05);
+}
+
+.masonry-caption {
+ padding: 1rem 1rem 0.5rem;
+}
+
+.masonry-caption h3 {
+ margin: 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #333;
+ line-height: 1.4;
+}
+
+.masonry-description {
+ padding: 0 1rem;
+ font-size: 0.9rem;
+ color: #666;
+ line-height: 1.5;
+}
+
+.masonry-meta {
+ padding: 0.75rem 1rem;
+ border-top: 1px solid #f0f0f0;
+ margin-top: 0.5rem;
+}
+
+.event-date {
+ font-size: 0.85rem;
+ color: #999;
+}
+
+.no-media {
+ text-align: center;
+ padding: 3rem 1rem;
+ color: #666;
+}
+
+.no-media p {
+ font-size: 1.1rem;
+}
+
diff --git a/assets/styles/landing.css b/assets/styles/04-pages/landing.css
similarity index 85%
rename from assets/styles/landing.css
rename to assets/styles/04-pages/landing.css
index 00d4629..ed632d7 100644
--- a/assets/styles/landing.css
+++ b/assets/styles/04-pages/landing.css
@@ -1,4 +1,3 @@
-
.center{ text-align: center; }
/* Eyebrow + lede */
@@ -7,12 +6,12 @@
letter-spacing: .12em;
font-size: .8rem;
color: var(--color-text-mid);
- margin: 0 0 1rem;
+ margin: 0 0 var(--spacing-3);
}
/* Hero split */
.ln-hero{
- margin-top: 100px;
+ margin-top: var(--spacing-8);
background: var(--color-bg);
color: var(--color-text);
}
@@ -27,7 +26,7 @@
}
/* Generic section shell */
-.ln-section{ position: relative; padding: 3.2rem 0; }
+.ln-section{ position: relative; padding: var(--section-spacing) 0; }
/* Split layout for features */
.ln-split{
@@ -35,9 +34,9 @@
grid-template-columns: 280px 1fr;
align-items: start;
}
-.ln-split__aside{ position: sticky; top: 24px; align-self: start; }
+.ln-split__aside{ position: sticky; top: var(--spacing-4); align-self: start; }
.ln-split__body .measure{ max-width: 70ch; }
-.cta-row{ margin-top: .6rem; }
+.cta-row{ margin-top: var(--spacing-2); }
/* Section palettes (alternating) */
.ln-section--search{
diff --git a/assets/styles/utilities.css b/assets/styles/05-utilities/utilities.css
similarity index 89%
rename from assets/styles/utilities.css
rename to assets/styles/05-utilities/utilities.css
index 8ef8f49..1cabdfa 100644
--- a/assets/styles/utilities.css
+++ b/assets/styles/05-utilities/utilities.css
@@ -1,4 +1,9 @@
-/* Utility classes (plain CSS) - spacing, text, layout, alerts, etc. */
+/**
+ * Utility Classes
+ * Bootstrap-style utility classes for quick styling adjustments
+ * Note: For main loading spinners, use .lds-dual-ring from spinner.css
+ * .spinner-border is for inline/button spinners
+ */
/* Spacing scale: 0=0, 1=.25rem, 2=.5rem, 3=1rem, 4=1.5rem, 5=3rem */
.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}
@@ -9,13 +14,15 @@
.mx-0{margin-left:0!important;margin-right:0!important}.mx-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-3{margin-left:1rem!important;margin-right:1rem!important}.mx-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-5{margin-left:3rem!important;margin-right:3rem!important}
.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}
.mx-auto{margin-left:auto!important;margin-right:auto!important}
+
/* Display & layout */
.d-flex{display:flex!important;flex-direction:column}
.d-inline{display:inline!important}
.d-block{display:block!important}
.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}
-.flex-row{flex-direction:row} .flex-wrap{flex-wrap: wrap}
+.flex-row{flex-direction:row}
+.flex-wrap{flex-wrap: wrap}
.justify-content-between{justify-content:space-between!important}
.justify-content-center{justify-content:center!important}
.align-items-center{align-items:center!important}
@@ -43,7 +50,7 @@
/* Buttons - sizes only (base buttons defined elsewhere) */
.btn.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.25;border-radius:.2rem}
-/* Spinner (bootstrap-like) */
+/* Inline spinner (for buttons, etc.) */
.spinner-border{display:inline-block;width:1.5rem;height:1.5rem;border:.2em solid currentColor;border-right-color:transparent;border-radius:50%;animation:spinner-border .75s linear infinite}
.spinner-border-sm{width:1rem;height:1rem;border-width:.15em}
@keyframes spinner-border{to{transform:rotate(360deg)}}
@@ -52,4 +59,4 @@
details>summary{cursor:pointer}
/* Text truncation with ellipsis */
-.line-clamp-5 {display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 5;overflow: hidden;text-overflow: ellipsis;}
+.line-clamp-5{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:5;overflow:hidden;text-overflow:ellipsis}
diff --git a/assets/styles/app.css b/assets/styles/app.css
deleted file mode 100644
index 3a5097b..0000000
--- a/assets/styles/app.css
+++ /dev/null
@@ -1,520 +0,0 @@
-body {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
- max-width: 100%;
- background-color: var(--color-bg);
- color: var(--color-text);
- font-family: var(--font-family), sans-serif;
- margin: 0;
- padding: 0;
- line-height: 1.6;
-}
-
-h1, h2, h3, h4, h5, h6 {
- font-family: var(--heading-font), serif;
- font-weight: 600;
- line-height: 1.1;
- color: var(--color-primary);
- margin: 30px 0 10px;
-}
-
-h1 {
- font-size: 3.2rem;
- margin-top: 0.25em;
- font-weight: 300;
-}
-
-h1.brand {
- font-family: var(--brand-font), serif;
- color: var(--color-primary);
- font-size: 3.2rem;
-}
-
-@media screen and (max-width: 600px) {
- h1.brand {
- font-size: 2.3rem;
- }
-}
-
-h1.brand a {
- color: var(--brand-color);
-}
-
-h1:not(.brand) > a:hover {
- text-decoration: none;
- font-weight: 500;
-}
-
-h2 {
- font-size: 2.2rem;
-}
-
-h2.brand {
- font-family: var(--brand-font), serif;
- color: var(--color-primary);
-}
-
-h3 {
- font-size: 2rem;
-}
-
-h4 {
- font-size: 1.9rem;
-}
-
-h5 {
- font-size: 1.75rem;
-}
-
-h6 {
- font-size: 1.5rem;
-}
-
-p {
- margin: 0 0 15px;
-}
-
-aside h1 {
- font-size: 1.2rem;
-}
-
-aside h2 {
- font-size: 1.1rem;
-}
-
-aside p.lede {
- font-size: 1rem;
-}
-
-.lede {
- font-family: var(--main-body-font), serif;
- font-size: 1.6rem;
- word-wrap: break-word;
- font-weight: 300;
-}
-
-strong:not(>h2), .strong {
- color: var(--color-primary);
-}
-
-.hidden {
- display: none;
-}
-
-a {
- color: var(--color-secondary);
- text-decoration: none;
-}
-
-a:hover {
- text-decoration: underline;
-}
-
-.card a:hover {
- text-decoration: none;
- color: var(--color-text);
- cursor: pointer;
-}
-
-.card a:hover h2 {
- color: var(--color-text);
-}
-
-img {
- max-width: 100%;
- height: auto;
-}
-
-svg.icon {
- width: 2em;
- height: 2em;
-}
-
-.divider {
- border: 2px solid var(--color-primary);
- margin: 20px 0;
-}
-
-.hashtag {
- color: var(--color-secondary);
-}
-
-.card {
- background-color: var(--color-bg);
- color: var(--color-text);
- padding: 0;
- margin: 0 0 2rem 0;
- border-radius: 0; /* Sharp edges */
-}
-
-.featured-cat {
- border-bottom: 2px solid var(--color-border);
- padding-left: 10px;
-}
-
-.featured-list {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
-}
-
-
-.featured-list > * {
- box-sizing: border-box; /* so padding/border don't break the layout */
- margin-bottom: 10px;
- padding: 10px;
-}
-
-@media (max-width: 1024px) {
- .featured-list {
- flex-direction: column !important;
- }
-
- .featured-list .card-header {
- margin-top: 20px;
- }
-
- .featured-list .card {
- border-bottom: 1px solid var(--color-border) !important;
- }
-
- .featured-list > * {
- margin-bottom: 10px;
- padding: 0;
- }
-}
-div:nth-child(odd) .featured-list {
- flex-direction: row-reverse;
-}
-
-.featured-list div:first-child {
- flex: 0 0 66%; /* each item takes up 50% width = 2 columns */
-}
-
-.featured-list div:last-child {
- flex: 0 0 34%; /* each item takes up 50% width = 2 columns */
-}
-
-.featured-list h2.card-title {
- font-size: 1.5rem;
-}
-
-.featured-list p.lede {
- font-size: 1.4rem;
-}
-
-.featured-list .card {
- margin-bottom: 20px;
-}
-
-.featured-list .card:not(:last-child) {
- border-bottom: 1px solid var(--color-border);
-}
-
-.article-list .metadata {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: baseline;
-}
-
-.article-list .metadata p {
- margin: 0;
-}
-
-.truncate {
- display: -webkit-box;
- -webkit-line-clamp: 3; /* limit to 3 lines */
- -webkit-box-orient: vertical;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.card.bordered {
- border: 2px solid var(--color-border);
-}
-
-.card-header {
- margin: 10px 0;
-}
-
-.header__image {
- position: relative;
- width: 100%;
- overflow: hidden; /* Ensures any overflow is hidden */
-}
-
-.header__image::before {
- content: "";
- display: block;
- padding-top: 56.25%; /* 16:9 aspect ratio (9 / 16 * 100 = 56.25%) */
-}
-
-.header__image img {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- object-fit: cover; /* Ensures the image covers the entire area while maintaining its aspect ratio */
-}
-
-.card-body {
- font-size: 1rem;
-}
-
-.card-footer {
- border-top: 1px solid var(--color-border);
- margin: 20px 0;
-}
-
-.header {
- text-align: center;
- z-index: 1000; /* Ensure it stays on top */
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: center;
- background-color: var(--color-bg); /* Black background */
- border-bottom: 1px solid var(--color-border);
-}
-
-.header .container {
- display: flex;
- flex-direction: column;
-}
-
-.header__categories ul {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- gap: 20px;
- padding: 0;
-}
-
-.header__categories li {
- list-style: none;
-}
-
-.header__categories li a:hover {
- text-decoration: none;
- font-weight: bold;
-}
-
-.header__categories a.active {
- font-weight: bold;
-}
-
-.header__logo h1 {
- font-weight: normal;
-}
-
-.header__logo img {
- height: 40px; /* Adjust the height as needed */
-}
-
-.header__logo a:hover {
- text-decoration: none;
-}
-
-.header__user {
- position: relative;
- display: flex;
- align-items: center;
-}
-
-.header__avatar img {
- height: 40px; /* Adjust the avatar size as needed */
- width: 40px;
- border-radius: 50%;
- cursor: pointer;
-}
-
-.header__dropdown {
- display: none;
- position: absolute;
- top: 50px; /* Adjust this depending on the header.html.twig height */
- right: 0;
- background-color: var(--color-text); /* White dropdown */
- border: 2px solid var(--color-bg); /* Black border */
- list-style: none;
- padding: 10px 0;
- z-index: 1000;
- border-radius: 0; /* Sharp edges */
-}
-
-.header__dropdown ul {
- margin: 0;
- padding: 0;
-}
-
-.header__dropdown li {
- padding: 10px 20px;
-}
-
-.header__dropdown li a {
- color: var(--color-bg); /* Black text */
- text-decoration: none;
-}
-
-.header__dropdown li a:hover {
- background-color: var(--color-bg); /* Black background on hover */
- color: var(--color-text); /* White text on hover */
- display: block;
-}
-
-footer p {
- margin: 0;
-}
-
-footer a {
- color: var(--color-text-contrast);
-}
-
-/* Tags container */
-.tags {
- margin: 10px 0;
- display: flex;
- flex-wrap: wrap; /* Allows tags to wrap to the next line if needed */
- gap: 10px; /* Adds spacing between individual tags */
-}
-
-/* Individual tag */
-.tag {
- background-color: var(--color-bg-light);
- color: var(--color-text-mid);
- padding: 3px 6px; /* Padding around the tag text */
- border-radius: 20px; /* Rounded corners (pill-shaped) */
- font-size: 0.75em; /* Slightly smaller text */
- cursor: pointer; /* Cursor turns to pointer for clickable tags */
- text-decoration: none; /* Removes any text decoration (e.g., underline) */
- display: inline-block; /* Makes sure each tag behaves like a block with padding */
- transition: background-color 0.3s ease; /* Smooth hover effect */
-}
-
-/*!* Hover effect for tags *!*/
-/*.tag:hover {*/
-/* color: var(--color-text-contrast);*/
-/*}*/
-
-/* Optional: Responsive adjustments for smaller screens */
-@media (max-width: 768px) {
- .tag {
- font-size: 0.8em; /* Slightly smaller text for mobile */
- }
-}
-
-
-
-.card.card__horizontal {
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- h1 {
- font-size: 2rem;
- }
-
- .card-content {
- flex: 1;
- margin-right: 30px;
- padding: 0 8px;
- }
-
- .card-image img {
- width: 220px;
- max-height: 220px;
- object-fit: contain;
- }
-}
-
-.article__image img {
- margin: 1rem 0;
- width: 100%;
-}
-
-.badge {
- background-color: var(--color-primary);
- color: var(--color-bg);
- padding: 3px 8px;
- border-radius: 20px;
- font-family: var(--font-family), sans-serif;
- font-weight: bold;
- font-size: 0.65em;
- text-transform: uppercase;
- margin-right: 5px;
- vertical-align: super;
-}
-
-.badge.badge__secondary {
- background-color: var(--color-secondary);
-}
-
-.avatar {
- width: 24px; /* Adjust the size as needed */
- height: 24px; /* Adjust the size as needed */
- border-radius: 50%; /* Makes the image circular */
- object-fit: cover; /* Ensures the image scales correctly */
- display: inline-block;
- vertical-align: middle;
-}
-
-.alert {
- padding: 10px 20px; /* Padding around the text */
- border-radius: 5px; /* Rounded corners */
- margin: 20px 0; /* Spacing around the alert */
-}
-
-.alert.alert-success {
- background-color: var(--color-secondary);
- color: var(--color-text-contrast);
-}
-
-/* Tabs Container */
-.nav-tabs {
- display: flex; /* Arrange items in a row */
- justify-content: center;
- padding: 0; /* Remove padding */
- margin: 0; /* Remove margin */
- list-style: none; /* Remove list item styling */
-}
-
-/* Individual Tab Item */
-.nav-tabs .nav-item {
- margin: 0; /* No margin around list items */
-}
-
-/* NON-Active Tab */
-.nav-tabs .nav-link {
- color: var(--color-text);
- background-color: transparent;
- border: none;
-}
-
-
-/* Active Tab */
-.nav-tabs .nav-link.active {
- color: var(--color-text-contrast);
- background-color: var(--color-primary);
- font-weight: bold;
-}
-
-/* Content Container */
-.tab-content {
- padding: 15px; /* Spacing inside the content */
- border-top: none; /* Remove border overlap with active tab */
-}
-
-/* Quill editor */
-#editor {
- height: 400px;
- margin-bottom: 20px;
-}
-
-/* Search */
-label.search {
- width: 100%;
- justify-content: center;
- margin-bottom: 15px;
-}
diff --git a/assets/styles/app.scss b/assets/styles/app.scss
deleted file mode 100644
index a313524..0000000
--- a/assets/styles/app.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-// ...existing code...
-@import "components/nostr_previews";
-// ...existing code...
diff --git a/assets/styles/components/_nostr_previews.scss b/assets/styles/components/_nostr_previews.scss
deleted file mode 100644
index fe3dc65..0000000
--- a/assets/styles/components/_nostr_previews.scss
+++ /dev/null
@@ -1,52 +0,0 @@
-.nostr-preview {
- margin-top: 0.5rem;
-
- .nostr-event-preview, .nostr-profile-preview {
- border-left: 3px solid #6c5ce7;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
-
- .nostr-profile-preview {
- border-left-color: #00b894;
- }
-
- .card-title {
- margin-bottom: 0.5rem;
- font-size: 1rem;
- }
-
- .card-text {
- font-size: 0.9rem;
- }
-
- .card-footer {
- padding: 0.5rem 1rem;
- }
-}
-
-.nostr-previews {
- h6 {
- font-size: 0.9rem;
- margin-bottom: 1rem;
- }
-
- .preview-container {
- // For multiple previews
- max-height: 500px;
- overflow-y: auto;
- padding-right: 10px;
- }
-}
-
-// Style for nostr links in text
-.nostr-link {
- color: #6c5ce7;
- background-color: rgba(108, 92, 231, 0.1);
- padding: 0 3px;
- border-radius: 3px;
- text-decoration: none;
-
- &:hover {
- background-color: rgba(108, 92, 231, 0.2);
- }
-}
diff --git a/src/Controller/AuthorController.php b/src/Controller/AuthorController.php
index 480df6e..eaed478 100644
--- a/src/Controller/AuthorController.php
+++ b/src/Controller/AuthorController.php
@@ -10,12 +10,59 @@ use Elastica\Query\Terms;
use Exception;
use FOS\ElasticaBundle\Finder\FinderInterface;
use swentel\nostr\Key\Key;
+use swentel\nostr\Nip19\Nip19Helper;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class AuthorController extends AbstractController
{
+ /**
+ * @throws Exception
+ */
+ #[Route('/p/{npub}/media', name: 'author-media', requirements: ['npub' => '^npub1.*'])]
+ public function media($npub, NostrClient $nostrClient, RedisCacheService $redisCacheService): Response
+ {
+ $keys = new Key();
+ $pubkey = $keys->convertToHex($npub);
+
+ $author = $redisCacheService->getMetadata($npub);
+
+ // Retrieve picture events (kind 20) for the author
+ try {
+ $pictureEvents = $nostrClient->getPictureEventsForPubkey($npub, 30);
+ } catch (Exception $e) {
+ $pictureEvents = [];
+ }
+
+ // Deduplicate by event ID
+ $uniqueEvents = [];
+ foreach ($pictureEvents as $event) {
+ if (!isset($uniqueEvents[$event->id])) {
+ $uniqueEvents[$event->id] = $event;
+ }
+ }
+
+ // Convert back to indexed array and sort by date (newest first)
+ $pictureEvents = array_values($uniqueEvents);
+ usort($pictureEvents, function ($a, $b) {
+ return $b->created_at <=> $a->created_at;
+ });
+
+ // Encode event IDs as note1... for each event
+ foreach ($pictureEvents as $event) {
+ $nip19 = new Nip19Helper(); // The NIP-19 helper class.
+ $event->noteId = $nip19->encodeNote($event->id);
+ }
+
+ return $this->render('pages/author-media.html.twig', [
+ 'author' => $author,
+ 'npub' => $npub,
+ 'pictureEvents' => $pictureEvents,
+ 'is_author_profile' => true,
+ ]);
+ }
+
/**
* @throws Exception
*/
diff --git a/src/Controller/EventController.php b/src/Controller/EventController.php
index 4a6a3d1..7c5aadd 100644
--- a/src/Controller/EventController.php
+++ b/src/Controller/EventController.php
@@ -24,7 +24,7 @@ class EventController extends AbstractController
/**
* @throws Exception
*/
- #[Route('/e/{nevent}', name: 'nevent', requirements: ['nevent' => '^nevent1.*'])]
+ #[Route('/e/{nevent}', name: 'nevent', requirements: ['nevent' => '^(nevent|note)1.*'])]
public function index($nevent, NostrClient $nostrClient, RedisCacheService $redisCacheService, NostrLinkParser $nostrLinkParser, LoggerInterface $logger): Response
{
$logger->info('Accessing event page', ['nevent' => $nevent]);
diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php
index 2a961b6..155fd64 100644
--- a/src/Service/NostrClient.php
+++ b/src/Service/NostrClient.php
@@ -516,6 +516,36 @@ class NostrClient
});
}
+ /**
+ * Get picture events (kind 20) for a specific author
+ * @throws \Exception
+ */
+ public function getPictureEventsForPubkey(string $ident, int $limit = 20): array
+ {
+ // Add user relays to the default set
+ $authorRelays = $this->getTopReputableRelaysForAuthor($ident);
+ // Create a RelaySet from the author's relays
+ $relaySet = $this->defaultRelaySet;
+ if (!empty($authorRelays)) {
+ $relaySet = $this->createRelaySet($authorRelays);
+ }
+
+ // Create request for kind 20 (picture events)
+ $request = $this->createNostrRequest(
+ kinds: [20], // NIP-68 Picture events
+ filters: [
+ 'authors' => [$ident],
+ 'limit' => $limit
+ ],
+ relaySet: $relaySet
+ );
+
+ // Process the response and return raw events
+ return $this->processResponse($request->send(), function($event) {
+ return $event; // Return the raw event
+ });
+ }
+
public function getArticles(array $slugs): array
{
$articles = [];
diff --git a/templates/admin/analytics.html.twig b/templates/admin/analytics.html.twig
index 6f8fe1c..94f420d 100644
--- a/templates/admin/analytics.html.twig
+++ b/templates/admin/analytics.html.twig
@@ -22,7 +22,7 @@
Route
- #
+ #
| Title | @@ -12,7 +12,7 @@||
|---|---|---|
| {{ article.title }} | {{ article.summary|slice(0, 100) }}{% if article.summary|length > 100 %}...{% endif %} | @@ -20,11 +20,10 @@ 30023:{{ article.pubkey }}:{{ article.slug }} + data-action="click->copy-to-clipboard#copyToClipboard">Copy to Clipboard - |