forums styling

This commit is contained in:
Andrew Brown 2025-09-16 00:55:31 -07:00
parent a7178ab3a6
commit ec9fa08b81
55 changed files with 6246 additions and 304 deletions

View File

@ -13,3 +13,8 @@
*= require font-awesome
*= require_tree .
*/
/* Alpine.js x-cloak - hide elements until Alpine loads */
[x-cloak] {
display: none !important;
}

View File

@ -0,0 +1,217 @@
// Currently Online Widget Styling
// A fixed position tab in bottom-right that expands to show online users
.thredded--currently-online {
position: fixed;
bottom: 0;
right: 20px;
z-index: 1000;
background: white;
border: 1px solid #e5e7eb;
border-bottom: none;
border-radius: 8px 8px 0 0;
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1), 0 -2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
min-width: 250px;
max-width: 350px;
// Collapsed state (default)
&[x-data] {
// This will be controlled by Alpine.js
}
// Header (always visible)
header {
cursor: pointer;
padding: 12px 16px;
background: #3b82f6; // bg-notebook-blue
border-radius: 7px 7px 0 0;
user-select: none;
&:hover {
background: #2563eb; // Slightly darker on hover
}
}
.thredded--currently-online--title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: white;
display: flex;
align-items: center;
justify-content: space-between;
// Add count badge
&::after {
content: attr(data-count);
background: rgba(255, 255, 255, 0.2);
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
margin-left: 8px;
}
// Expand/collapse indicator
&::before {
content: '';
font-size: 10px;
margin-right: 8px;
transition: transform 0.3s ease;
display: inline-block;
}
&.collapsed::before {
transform: rotate(-90deg);
}
}
// User list container
.thredded--currently-online--users {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
margin: 0;
padding: 0;
list-style: none;
background: white;
border-top: 1px solid #f3f4f6;
&.expanded {
max-height: 400px;
overflow-y: auto;
padding: 8px 0;
}
}
// Individual user item
.thredded--currently-online--user {
padding: 0;
margin: 0;
a {
display: flex;
align-items: center;
padding: 8px 16px;
text-decoration: none;
color: #374151;
font-size: 14px;
transition: background-color 0.2s ease;
&:hover {
background-color: #f9fafb;
color: #3b82f6;
}
}
}
// User avatar
.thredded--currently-online--avatar {
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 12px;
border: 2px solid #10b981;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
// Online indicator dot
.thredded--currently-online--user a::after {
content: '';
width: 8px;
height: 8px;
background: #10b981;
border-radius: 50%;
margin-left: auto;
animation: pulse 2s infinite;
}
// Scrollbar styling for user list
.thredded--currently-online--users::-webkit-scrollbar {
width: 6px;
}
.thredded--currently-online--users::-webkit-scrollbar-track {
background: #f3f4f6;
border-radius: 3px;
}
.thredded--currently-online--users::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 3px;
&:hover {
background: #9ca3af;
}
}
}
// Pulse animation for online indicator
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
}
70% {
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
// Dark mode support
.dark .thredded--currently-online {
background: #1f2937;
border-color: #374151;
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.3);
header {
background: #1e40af; // Darker blue for dark mode
&:hover {
background: #1e3a8a; // Even darker on hover
}
}
.thredded--currently-online--users {
background: #1f2937;
border-top-color: #374151;
}
.thredded--currently-online--user a {
color: #d1d5db;
&:hover {
background-color: #111827;
color: #a78bfa;
}
}
.thredded--currently-online--avatar {
border-color: #10b981;
}
}
// Mobile responsiveness
@media (max-width: 640px) {
.thredded--currently-online {
right: 10px;
min-width: 160px;
max-width: 200px;
.thredded--currently-online--title {
font-size: 13px;
}
.thredded--currently-online--user a {
font-size: 13px;
padding: 6px 12px;
}
.thredded--currently-online--avatar {
width: 28px;
height: 28px;
margin-right: 10px;
}
}
}

View File

@ -0,0 +1,268 @@
// Forum Messageboards - Professional Literary Design
// Minimal, sophisticated design for professional and indie authors
// Messageboard card - clean manuscript aesthetic
.messageboard-card {
position: relative;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 3px;
transition: all 0.15s ease;
overflow: hidden;
// Subtle depth indicator - hint at stack without being literal
border-bottom: 2px solid #e5e7eb;
// Clean, professional shadow
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
// Refined hover state - subtle elevation
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
border-bottom-color: #d1d5db;
h3 {
color: #3b82f6; // notebook-blue on hover for title
}
}
// Active/focus state
&:focus-within {
outline: 2px solid #3b82f6;
outline-offset: -1px;
}
}
// Compact, professional spacing
.messageboard-compact {
// Header section
.messageboard-header {
padding: 0.875rem; // 14px
h3 {
font-size: 0.9375rem; // 15px
line-height: 1.25rem;
font-weight: 600;
margin: 0;
color: #111827;
transition: color 0.15s ease;
letter-spacing: -0.01em;
}
}
// Description - subtle and professional
.messageboard-description {
font-size: 0.8125rem; // 13px
line-height: 1.25rem;
margin: 0.375rem 0;
color: #6b7280;
// Single line with ellipsis
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// Stats section - minimal
.messageboard-stats {
font-size: 0.75rem; // 12px
color: #9ca3af;
margin-top: 0.625rem;
font-weight: 500;
letter-spacing: 0.025em;
.material-icons {
font-size: 0.875rem; // 14px
vertical-align: text-bottom;
opacity: 0.7;
}
}
// Footer - subtle separator
.messageboard-footer {
padding: 0.625rem 0.875rem; // 10px 14px
background: #fafafa;
border-top: 1px solid #f3f4f6;
font-size: 0.75rem; // 12px
color: #6b7280;
span {
font-weight: 500;
}
}
}
// Group titles - typography-focused
.messageboard-group-title {
margin: 1.25rem 0 0.875rem 0;
font-size: 0.75rem; // 12px
font-weight: 700;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.05em;
// Remove decorative line - let typography do the work
&::before {
display: none;
}
// First group spacing
&:first-child {
margin-top: 0;
}
a {
color: inherit;
text-decoration: none;
&:hover {
color: #4b5563;
}
}
}
// Grid with professional spacing
.messageboards-grid {
gap: 1rem; // 16px - gives breathing room
@media (min-width: 768px) {
gap: 1.25rem; // 20px on larger screens
}
}
// Locked indicator - subtle and inline
.messageboard-locked {
.messageboard-header h3 {
.material-icons {
font-size: 0.875rem;
opacity: 0.5;
vertical-align: middle;
margin-right: 0.25rem;
}
}
}
// Unread badge - minimal
.messageboard-unread-badge {
padding: 0.125rem 0.5rem;
font-size: 0.6875rem; // 11px
font-weight: 600;
border-radius: 3px;
background: #f3f4f6;
color: #4b5563;
// Blue for followed topics
&.followed {
background: #eff6ff;
color: #3b82f6;
}
// Blue for new topics
&.new {
background: #eff6ff;
color: #2563eb;
}
}
// Page header - clean and professional
.forum-page-header {
padding: 1.5rem 0 1rem;
border-bottom: 1px solid #f3f4f6;
margin-bottom: 1.5rem;
h1 {
font-size: 1.75rem; // 28px
line-height: 2.25rem;
font-weight: 700;
letter-spacing: -0.025em;
color: #111827;
.material-icons {
font-size: 1.75rem;
opacity: 0.8;
vertical-align: middle;
margin-right: 0.5rem;
}
}
p {
font-size: 0.9375rem; // 15px
margin-top: 0.375rem;
color: #6b7280;
}
}
// Dark mode - maintain professionalism
.dark {
.messageboard-card {
background: #1f2937;
border-color: #374151;
border-bottom-color: #374151;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
&:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
border-bottom-color: #4b5563;
h3 {
color: #a78bfa;
}
}
}
.messageboard-compact {
.messageboard-header h3 {
color: #f3f4f6;
}
.messageboard-description {
color: #9ca3af;
}
.messageboard-stats {
color: #6b7280;
}
.messageboard-footer {
background: #111827;
border-top-color: #374151;
color: #9ca3af;
}
}
.messageboard-group-title {
color: #9ca3af;
a:hover {
color: #d1d5db;
}
}
.messageboard-unread-badge {
background: #374151;
color: #d1d5db;
&.followed {
background: rgba(167, 139, 250, 0.1);
color: #a78bfa;
}
&.new {
background: rgba(59, 130, 246, 0.1);
color: #60a5fa;
}
}
.forum-page-header {
border-bottom-color: #374151;
h1 {
color: #f3f4f6;
}
p {
color: #9ca3af;
}
}
}

View File

@ -0,0 +1,57 @@
// Forum Navigation Styling
// This file contains minimal styling for forum navigation elements
// that can't be easily styled with Tailwind classes
.thredded-nav {
// Ensure nav items are properly contained
ul {
display: flex;
align-items: center;
height: 100%;
margin: 0;
padding: 0;
list-style: none;
}
li {
display: flex;
align-items: center;
height: 100%;
margin: 0;
}
// Style for navigation icons
.material-icons {
font-size: 18px;
vertical-align: middle;
}
// Ensure links fill their containers
a {
display: flex;
align-items: center;
height: 100%;
text-decoration: none;
}
// Current page indicator
.thredded--is-current {
background-color: rgba(59, 130, 246, 0.1); // blue-50 equivalent
border-bottom: 2px solid rgb(59, 130, 246); // blue-500
}
}
// Ensure the thredded container doesn't overflow
#thredded--container {
max-width: 100%;
}
.thredded--container {
max-width: 100%;
}
// Forum post content sizing
.thredded--post--content {
font-size: 14px;
line-height: 1.6;
}

View File

@ -0,0 +1,415 @@
// Forum Topics - Collapsible New Discussion Form
// Progressive disclosure pattern for starting new discussions
// New topic form container
.thredded--new-topic-form {
position: relative;
margin-bottom: 1.5rem;
transition: all 0.3s ease;
// Wrapper for Alpine.js states
&[x-data] {
// Initial collapsed state styles handled by Alpine
}
}
// Collapsed state - single input field
.new-topic-collapsed {
position: relative;
.collapsed-input {
width: 100%;
padding: 1.5rem 1rem 1.5rem 2.75rem;
font-size: 0.9375rem;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 6px;
cursor: text;
transition: all 0.2s ease;
color: #6b7280;
&:hover {
border-color: #d1d5db;
background: #fafafa;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
color: #111827;
}
// Icon
&::before {
content: '';
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
width: 1.25rem;
height: 1.25rem;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='M12 5v14M5 12h14'/%3E%3C/svg%3E") no-repeat center;
background-size: contain;
}
}
// Fake placeholder
.collapsed-placeholder {
position: absolute;
left: 2.75rem;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
pointer-events: none;
transition: opacity 0.2s ease;
}
}
// Expanded state - full form
.new-topic-expanded {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
overflow: hidden;
// Form header
.form-header {
padding: 1rem 1.25rem 0.75rem;
border-bottom: 1px solid #f3f4f6;
background: #fafafa;
.form-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
display: flex;
align-items: center;
.material-icons {
font-size: 1rem;
margin-right: 0.5rem;
opacity: 0.7;
}
}
}
// Form content area
.form-content {
padding: 1.25rem;
}
// Override thredded's default form list styles
.thredded--form-list {
margin: 0;
padding: 0;
list-style: none;
li {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
// Title field
li.title {
input[type="text"] {
width: 100%;
padding: 0.625rem 0.875rem;
font-size: 1rem;
border: 1px solid #d1d5db;
border-radius: 4px;
transition: all 0.15s ease;
&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
&::placeholder {
color: #9ca3af;
}
}
label {
display: block;
font-size: 0.8125rem;
font-weight: 500;
color: #4b5563;
margin-bottom: 0.375rem;
}
}
// Content textarea
textarea {
width: 100%;
min-height: 120px;
padding: 0.625rem 0.875rem;
font-size: 0.9375rem;
border: 1px solid #d1d5db;
border-radius: 4px;
resize: vertical;
transition: all 0.15s ease;
font-family: inherit;
&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
min-height: 150px;
}
&::placeholder {
color: #9ca3af;
}
}
// Category select
li.category {
select {
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
border: 1px solid #d1d5db;
border-radius: 4px;
background: white;
cursor: pointer;
&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
}
}
}
// Form actions
.form-actions {
padding: 1rem 1.25rem;
background: #fafafa;
border-top: 1px solid #f3f4f6;
display: flex;
justify-content: space-between;
align-items: center;
.left-actions {
display: flex;
gap: 0.5rem;
}
.right-actions {
display: flex;
gap: 0.5rem;
}
// Buttons
.thredded--form--submit {
padding: 0.5rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.15s ease;
background: #3b82f6;
color: white;
&:hover:not(:disabled) {
background: #2563eb;
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.btn-cancel {
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 4px;
border: 1px solid #d1d5db;
background: white;
color: #6b7280;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: #f9fafb;
border-color: #9ca3af;
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.1);
}
}
.btn-preview {
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 4px;
border: 1px solid #e5e7eb;
background: white;
color: #6b7280;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
.material-icons {
font-size: 1rem;
margin-right: 0.25rem;
}
&:hover {
background: #f9fafb;
}
&.active {
background: #eff6ff;
border-color: #3b82f6;
color: #2563eb;
}
}
}
}
// Preview area
.thredded--preview-area {
margin: 1rem 0;
padding: 1rem;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 4px;
min-height: 100px;
&:empty::before {
content: 'Preview will appear here...';
color: #9ca3af;
font-style: italic;
}
}
// Transition classes for smooth animations
.form-expand-enter {
opacity: 0;
transform: translateY(-10px);
}
.form-expand-enter-active {
opacity: 1;
transform: translateY(0);
transition: all 0.3s ease;
}
.form-expand-leave {
opacity: 1;
transform: translateY(0);
}
.form-expand-leave-active {
opacity: 0;
transform: translateY(-10px);
transition: all 0.2s ease;
}
// Dark mode support
.dark {
.new-topic-collapsed {
.collapsed-input {
background: #1f2937;
border-color: #374151;
color: #9ca3af;
&:hover {
background: #111827;
border-color: #4b5563;
}
&:focus {
border-color: #3b82f6;
color: #f3f4f6;
}
}
.collapsed-placeholder {
color: #6b7280;
}
}
.new-topic-expanded {
background: #1f2937;
border-color: #374151;
.form-header {
background: #111827;
border-bottom-color: #374151;
.form-title {
color: #d1d5db;
}
}
.thredded--form-list {
input[type="text"],
textarea,
select {
background: #111827;
border-color: #374151;
color: #f3f4f6;
&:focus {
border-color: #3b82f6;
}
&::placeholder {
color: #6b7280;
}
}
label {
color: #9ca3af;
}
}
.form-actions {
background: #111827;
border-top-color: #374151;
.btn-cancel,
.btn-preview {
background: #1f2937;
border-color: #374151;
color: #9ca3af;
&:hover {
background: #374151;
}
}
}
}
.thredded--preview-area {
background: #111827;
border-color: #374151;
color: #d1d5db;
&:empty::before {
color: #6b7280;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,546 @@
// Private Message Compose Styling
// Clean, professional email composition interface
// Page background
#thredded--new-private-topic {
background: linear-gradient(180deg, #f9fafb 0%, #f3f4f6 100%);
min-height: calc(100vh - 200px);
}
// Main compose container
.compose-container {
max-width: 800px;
margin: 2.5rem auto 1.5rem;
padding: 0 1.5rem;
}
// Compose wrapper for card effect
.compose-wrapper {
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 2rem;
}
// Compose header
.compose-header {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
padding: 1.25rem 1.5rem;
border-bottom: none;
h1 {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin: 0;
display: flex;
align-items: center;
.material-icons {
font-size: 1.625rem;
margin-right: 0.625rem;
color: white;
opacity: 0.9;
}
}
.compose-subtitle {
margin-top: 0.25rem;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.85);
margin-left: 2.25rem;
}
}
// Compose form
.thredded--new-private-topic-form {
background: white;
border: none;
border-radius: 0;
padding: 0.5rem 0 0;
.thredded--form-list {
margin: 0;
padding: 0;
list-style: none;
li,
> div {
padding: 1rem 1.5rem;
border-bottom: 1px solid #f3f4f6;
margin: 0;
&:last-child {
border-bottom: none;
background: #f9fafb;
padding: 1rem 1.5rem;
margin-top: 0;
}
}
// Field labels
label {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 0.375rem;
&::after {
content: ':';
margin-left: 0.125rem;
}
}
// Title field
li.title {
input[type="text"] {
width: 100%;
padding: 0.625rem 0.875rem;
font-size: 1rem;
border: 1px solid #d1d5db;
border-radius: 6px;
transition: all 0.15s ease;
&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
&::placeholder {
color: #9ca3af;
}
}
}
// Recipients field
textarea[data-thredded-users-select] {
width: 100%;
min-height: 44px;
max-height: 120px;
padding: 0.625rem 1rem;
font-size: 0.9375rem;
border: 1px solid #d1d5db;
border-radius: 6px;
resize: vertical;
transition: all 0.15s ease;
font-family: inherit;
&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
min-height: 60px;
}
&::placeholder {
color: #9ca3af;
}
}
// Content field - target the textarea with ID
textarea#private_topic_content,
textarea#private_post_content,
textarea[name*="[content]"] {
width: 100%;
min-height: 180px;
padding: 1rem 1.125rem;
font-size: 1rem;
border: 1px solid #d1d5db;
border-radius: 8px;
resize: vertical;
transition: all 0.15s ease;
font-family: inherit;
line-height: 1.6;
background: #fafafa;
&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
min-height: 200px;
background: white;
}
&::placeholder {
color: #9ca3af;
}
}
}
}
// Recipients helper text
.recipients-helper {
margin-top: 0.375rem;
font-size: 0.75rem;
color: #6b7280;
.material-icons {
font-size: 0.875rem;
vertical-align: middle;
margin-right: 0.25rem;
}
}
// Selected users display
.selected-users {
margin-top: 0.75rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
.user-tag {
display: inline-flex;
align-items: center;
padding: 0.375rem 0.625rem;
background: #eff6ff;
border: 1px solid #3b82f6;
border-radius: 16px;
font-size: 0.8125rem;
color: #1e40af;
.remove-user {
margin-left: 0.375rem;
cursor: pointer;
color: #3b82f6;
font-weight: bold;
&:hover {
color: #1e40af;
}
}
}
}
// Form actions
.compose-actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
.left-actions {
display: flex;
gap: 0.5rem;
}
.right-actions {
display: flex;
gap: 0.5rem;
}
}
// Submit button
.thredded--form--submit {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.75rem;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9375rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
&:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.2);
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.material-icons {
font-size: 1.125rem;
margin-right: 0.5rem;
}
}
// Cancel/back button
.btn-cancel {
display: inline-flex;
align-items: center;
padding: 0.625rem 1.25rem;
background: white;
color: #6b7280;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: #f9fafb;
border-color: #d1d5db;
color: #374151;
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.1);
}
}
// Preview button
.btn-preview {
display: inline-flex;
align-items: center;
padding: 0.625rem 1.25rem;
background: white;
color: #6b7280;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
.material-icons {
font-size: 1rem;
margin-right: 0.375rem;
}
&:hover {
background: #f9fafb;
border-color: #d1d5db;
}
&.active {
background: #eff6ff;
border-color: #3b82f6;
color: #2563eb;
}
}
// Preview area
.thredded--preview-area {
margin-top: 1rem;
padding: 1rem;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 6px;
min-height: 100px;
.thredded--preview-area--title {
font-size: 0.75rem;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.025em;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}
.thredded--preview-area--post {
color: #374151;
line-height: 1.6;
&:empty::before {
content: 'Message preview will appear here...';
color: #9ca3af;
font-style: italic;
}
}
}
// Field errors
.thredded--form-field-errors {
margin-top: 0.375rem;
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
padding: 0.375rem 0.625rem !important;
background: #fef2f2;
border: 1px solid #fecaca !important;
border-radius: 4px;
color: #dc2626;
font-size: 0.8125rem;
margin-top: 0.25rem;
}
}
// Autocomplete dropdown
.textcomplete-dropdown {
background: white;
border: 1px solid #e5e7eb;
border-radius: 6px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
margin-top: 0.25rem;
max-height: 200px;
overflow-y: auto;
li {
padding: 0.5rem 0.75rem !important;
border-bottom: 1px solid #f3f4f6 !important;
cursor: pointer;
transition: background 0.15s ease;
&:hover,
&.active {
background: #f3f4f6;
}
&:last-child {
border-bottom: none !important;
}
}
}
// Dark mode support
.dark {
#thredded--new-private-topic {
background: linear-gradient(180deg, #111827 0%, #0f172a 100%);
}
.compose-wrapper {
background: #1f2937;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4);
}
.compose-header {
background: linear-gradient(135deg, #1e40af 0%, #1e3a8a 100%);
border-color: #374151;
h1 {
color: #f3f4f6;
.material-icons {
color: #60a5fa;
}
}
.compose-subtitle {
color: #9ca3af;
}
}
.thredded--new-private-topic-form {
background: #1f2937;
border-color: #374151;
.thredded--form-list {
li,
> div {
border-bottom-color: #374151;
&:last-child {
background: #111827;
}
}
label {
color: #d1d5db;
}
input[type="text"],
textarea {
background: #111827;
border-color: #374151;
color: #f3f4f6;
&:focus {
border-color: #3b82f6;
}
&::placeholder {
color: #6b7280;
}
}
}
}
.recipients-helper {
color: #9ca3af;
}
.selected-users {
.user-tag {
background: #1e293b;
border-color: #3b82f6;
color: #60a5fa;
}
}
.btn-cancel {
background: #1f2937;
border-color: #374151;
color: #9ca3af;
&:hover {
background: #374151;
}
}
.thredded--preview-area {
background: #111827;
border-color: #374151;
.thredded--preview-area--title {
color: #9ca3af;
border-bottom-color: #374151;
}
.thredded--preview-area--post {
color: #d1d5db;
&:empty::before {
color: #6b7280;
}
}
}
}
// Mobile responsiveness
@media (max-width: 768px) {
.compose-container {
margin: 1rem auto;
}
.compose-header {
padding: 1rem;
h1 {
font-size: 1.25rem;
}
}
.thredded--new-private-topic-form {
.thredded--form-list li {
padding: 1rem;
}
}
.compose-actions {
flex-direction: column;
.left-actions,
.right-actions {
width: 100%;
button {
flex: 1;
}
}
}
}

View File

@ -0,0 +1,524 @@
// Private Messages / Inbox Styling
// Modern, clean inbox design for private conversations
// Main container
.thredded--private-topics {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
// Page header
.private-messages-header {
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 2px solid #e5e7eb;
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
}
h1 {
font-size: 1.875rem;
font-weight: 700;
color: #111827;
margin: 0;
display: flex;
align-items: center;
.material-icons {
font-size: 2rem;
margin-right: 0.75rem;
color: #3b82f6;
}
}
.header-actions {
display: flex;
gap: 0.5rem;
.btn-compose {
display: inline-flex;
align-items: center;
padding: 0.625rem 1.25rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
transition: all 0.15s ease;
&:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.2);
}
.material-icons {
font-size: 1.125rem;
margin-right: 0.5rem;
}
}
}
}
// Inbox stats
.inbox-stats {
display: flex;
gap: 2rem;
margin-top: 0.75rem;
font-size: 0.875rem;
color: #6b7280;
.stat {
display: flex;
align-items: center;
gap: 0.5rem;
.material-icons {
font-size: 1rem;
opacity: 0.7;
}
strong {
color: #374151;
font-weight: 600;
}
}
}
// Messages list container
.messages-list {
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
// Individual message/conversation row
.thredded--topics--topic {
display: flex;
align-items: center;
padding: 1rem 1.25rem;
border-bottom: 1px solid #f3f4f6;
transition: all 0.15s ease;
position: relative;
&:hover {
background: #f9fafb;
}
&:last-child {
border-bottom: none;
}
// Unread indicator
&.thredded--topic-unread {
background: #fefce8;
border-left: 3px solid #facc15;
.thredded--topics--title {
font-weight: 600;
color: #111827;
}
&::before {
content: '';
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
background: #3b82f6;
border-radius: 50%;
}
}
}
// Participants avatars
.message-participants {
flex-shrink: 0;
margin-right: 1rem;
display: flex;
align-items: center;
.participant-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid white;
margin-right: -8px;
position: relative;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
&:first-child {
z-index: 3;
}
&:nth-child(2) {
z-index: 2;
}
&:nth-child(3) {
z-index: 1;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
// Initials for users without avatars
.initials {
font-size: 0.75rem;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
}
}
// More participants indicator
.more-participants {
margin-left: 0.25rem;
font-size: 0.75rem;
color: #6b7280;
font-weight: 500;
}
}
// Message content area
.message-content {
flex: 1;
min-width: 0;
.thredded--topics--title {
margin: 0 0 0.25rem 0;
font-size: 0.9375rem;
line-height: 1.4;
a {
color: #374151;
text-decoration: none;
&:hover {
color: #3b82f6;
}
}
}
// Message preview/excerpt
.message-preview {
font-size: 0.8125rem;
color: #6b7280;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 500px;
}
}
// Posts count badge
.thredded--topics--posts-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
padding: 0 6px;
background: #f3f4f6;
color: #6b7280;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
margin-right: 1rem;
flex-shrink: 0;
.thredded--topic-unread & {
background: #3b82f6;
color: white;
}
}
// Timestamp and metadata
.thredded--topics--updated-by {
display: flex;
flex-direction: column;
align-items: flex-end;
font-size: 0.75rem;
color: #9ca3af;
font-style: normal;
min-width: 100px;
time {
font-weight: 500;
}
.thredded--topics--participants {
margin-top: 0.25rem;
display: flex;
gap: 0.25rem;
a {
color: #6b7280;
text-decoration: none;
&:hover {
color: #3b82f6;
text-decoration: underline;
}
}
}
}
// Empty state
.thredded--empty {
text-align: center;
padding: 4rem 2rem;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
&::before {
content: '';
display: block;
width: 80px;
height: 80px;
margin: 0 auto 1.5rem;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='1.5'%3E%3Cpath d='M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z'/%3E%3C/svg%3E") no-repeat center;
background-size: contain;
opacity: 0.5;
}
.thredded--empty--title {
font-size: 1.125rem;
color: #374151;
margin-bottom: 0.75rem;
font-weight: 600;
}
p {
color: #6b7280;
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.thredded--button {
display: inline-flex;
align-items: center;
padding: 0.625rem 1.25rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
transition: all 0.15s ease;
&:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.2);
}
.material-icons {
font-size: 1.125rem;
margin-right: 0.5rem;
}
}
}
// Mark all read button
.thredded--button-wide {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
margin-top: 1rem;
padding: 0.625rem 1.25rem;
background: white;
color: #6b7280;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
transition: all 0.15s ease;
&:hover {
background: #f9fafb;
border-color: #d1d5db;
color: #374151;
}
}
// Pagination
.thredded--pagination-bottom {
margin-top: 2rem;
.pagination {
display: flex;
justify-content: center;
gap: 0.5rem;
a, span {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
border-radius: 4px;
text-decoration: none;
transition: all 0.15s ease;
}
a {
background: white;
border: 1px solid #e5e7eb;
color: #374151;
&:hover {
background: #f9fafb;
border-color: #d1d5db;
}
}
.current {
background: #3b82f6;
color: white;
border: 1px solid #3b82f6;
}
}
}
// Dark mode support
.dark {
.private-messages-header {
border-bottom-color: #374151;
h1 {
color: #f3f4f6;
.material-icons {
color: #60a5fa;
}
}
}
.inbox-stats {
color: #9ca3af;
.stat strong {
color: #d1d5db;
}
}
.messages-list {
background: #1f2937;
border-color: #374151;
}
.thredded--topics--topic {
border-bottom-color: #374151;
&:hover {
background: #111827;
}
&.thredded--topic-unread {
background: #1e293b;
border-left-color: #facc15;
}
}
.message-content {
.thredded--topics--title a {
color: #e5e7eb;
&:hover {
color: #60a5fa;
}
}
.message-preview {
color: #9ca3af;
}
}
.thredded--topics--posts-count {
background: #374151;
color: #d1d5db;
.thredded--topic-unread & {
background: #3b82f6;
color: white;
}
}
.thredded--empty {
background: #1f2937;
border-color: #374151;
.thredded--empty--title {
color: #e5e7eb;
}
p {
color: #9ca3af;
}
}
}
// Mobile responsiveness
@media (max-width: 768px) {
.private-messages-header {
.header-content {
flex-direction: column;
align-items: flex-start;
}
h1 {
font-size: 1.5rem;
}
}
.inbox-stats {
flex-wrap: wrap;
gap: 1rem;
}
.thredded--topics--topic {
padding: 0.875rem 1rem;
}
.message-participants {
margin-right: 0.75rem;
.participant-avatar {
width: 32px;
height: 32px;
}
}
.message-content {
.message-preview {
max-width: 100%;
}
}
.thredded--topics--updated-by {
min-width: auto;
font-size: 0.6875rem;
}
}

View File

@ -1,39 +1,27 @@
@import "thredded";
// Thredded Forum Overrides
// This file contains styling overrides for the Thredded forum gem
.thredded--topic-header--follow-info--follow {
position: static !important;
// Hide the SVG definition container completely
// These SVG icons are defined by Thredded but we use Material Icons instead
.thredded--svg-definitions {
display: none !important;
visibility: hidden !important;
position: absolute !important;
width: 0 !important;
height: 0 !important;
overflow: hidden !important;
}
.thredded--topic-header--follow-info--unfollow {
position: static !important;
// Fallback: If any SVGs somehow still display, keep them small
// This ensures no giant icons break the layout
svg {
max-width: 1.5rem;
max-height: 1.5rem;
}
#thredded--container {
margin-left: 32px !important;
}
.thredded--container {
margin: 0;
margin-left: 3em !important;
}
@media only screen and (min-width: 601px) {
.thredded--nav {
nav, nav .nav-wrapper i, nav a.sidenav-trigger, nav a.sidenav-trigger i {
height: inherit !important;
line-height: inherit !important;
}
}
}
.thredded--main-container {
max-width: 100%;
}
.thredded--post--content {
font-size: 12pt;
}
.thredded--post {
margin-bottom: 1em !important;
// Specific IDs for thredded icons (belt and suspenders approach)
#thredded-follow-icon,
#thredded-lock-icon,
#thredded-unfollow-icon {
display: none !important;
}

View File

@ -0,0 +1,31 @@
@import "thredded";
.thredded--topic-header--follow-info--follow {
position: static !important;
}
.thredded--topic-header--follow-info--unfollow {
position: static !important;
}
#thredded--container {
margin-left: 32px !important;
}
.thredded--container {
margin: 0;
margin-left: 3em !important;
}
.thredded--main-container {
max-width: 100%;
}
.thredded--post--content {
font-size: 12pt;
}
.thredded--post {
margin-bottom: 1em !important;
}

View File

@ -0,0 +1,540 @@
// Topic Show Page Styling
// Full-width header with narrower content column layout
// Topic header section - full width background
.topic-header-section {
background: #2196F3; // notebook-blue flat color
color: white;
margin: -2rem -9999px 1.5rem -9999px;
padding: 1.25rem 9999px;
.topic-header-container {
max-width: 1280px;
margin: 0 auto;
padding: 0 1.5rem;
}
// Topic title
.topic-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.625rem;
line-height: 1.3;
@media (max-width: 768px) {
font-size: 1.25rem;
}
.material-icons {
font-size: 1.25rem;
margin-right: 0.375rem;
vertical-align: middle;
opacity: 0.9;
}
}
// Metadata row
.topic-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
font-size: 0.875rem;
opacity: 0.95;
margin-bottom: 0.75rem;
.meta-item {
display: flex;
align-items: center;
.material-icons {
font-size: 0.875rem;
margin-right: 0.25rem;
opacity: 0.8;
}
a {
color: white;
text-decoration: underline;
text-underline-offset: 2px;
&:hover {
text-decoration-thickness: 2px;
}
}
&:not(:last-child)::after {
content: '|';
margin: 0 0.75rem;
opacity: 0.5;
}
}
}
// Categories
.topic-categories {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin-top: 0.5rem;
.category-badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
color: white;
&:hover {
background: rgba(255, 255, 255, 0.25);
}
}
}
// Follow/action buttons
.topic-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
button, a {
display: inline-flex;
align-items: center;
padding: 0.375rem 0.75rem;
background: transparent;
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: all 0.15s ease;
&:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.5);
}
.material-icons {
font-size: 0.875rem;
margin-right: 0.25rem;
}
&.following {
background: rgba(255, 255, 255, 0.15);
color: white;
border: 1px solid rgba(255, 255, 255, 0.4);
&:hover {
background: rgba(255, 255, 255, 0.25);
}
}
&.text-red-600 {
border-color: rgba(255, 255, 255, 0.3);
&:hover {
background: rgba(220, 38, 38, 0.2);
border-color: rgba(220, 38, 38, 0.5);
}
}
}
}
}
// Content area - narrower column
.topic-content-area {
max-width: 48rem; // 768px - narrower than the header
margin: 0 auto;
padding: 0 1.5rem;
// Posts container
.posts-container {
margin-bottom: 2rem;
}
}
// Individual post styling - Modern chat bubble design
.thredded--post {
margin-bottom: 1.5rem;
position: relative;
// Override default thredded styles
background: transparent;
border: none;
box-shadow: none;
overflow: visible;
// The speech bubble content styling
.prose {
font-size: 0.9375rem;
line-height: 1.65;
p {
margin-bottom: 0.75rem;
&:last-child {
margin-bottom: 0;
}
}
blockquote {
margin: 0.75rem 0;
padding-left: 1rem;
border-left: 3px solid #2196F3;
color: #6b7280;
font-style: italic;
background: #f9fafb;
padding: 0.75rem 1rem;
border-radius: 0.25rem;
}
code {
background: #f3f4f6;
padding: 0.125rem 0.375rem;
border-radius: 3px;
font-size: 0.875rem;
color: #d97706;
}
pre {
background: #1f2937;
color: #f3f4f6;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
margin: 0.75rem 0;
code {
background: transparent;
padding: 0;
color: inherit;
}
}
a {
color: #2196F3;
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.15s ease;
&:hover {
border-bottom-color: #2196F3;
}
}
ul, ol {
margin: 0.75rem 0;
padding-left: 1.5rem;
}
li {
margin: 0.25rem 0;
}
}
// First unread post indicator
&.thredded--unread--post {
.post-content-card {
border-bottom: 3px solid #2196F3;
padding-bottom: calc(1rem - 3px); // Adjust padding to account for border
}
}
}
// Post actions dropdown styling
.thredded--post--dropdown {
position: relative;
display: inline-block;
.thredded--post--dropdown--toggle {
width: 20px;
height: 20px;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.15s ease;
&:hover {
opacity: 1;
}
}
.thredded--post--dropdown--actions {
position: absolute;
right: 0;
top: 100%;
margin-top: 0.25rem;
background: white;
border: 1px solid #e5e7eb;
border-radius: 6px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
min-width: 140px;
display: none;
z-index: 10;
a {
display: block;
padding: 0.5rem 0.75rem;
font-size: 0.8125rem;
color: #6b7280;
text-decoration: none;
transition: all 0.15s ease;
&:hover {
background: #f9fafb;
color: #2196F3;
}
&:first-child {
border-radius: 6px 6px 0 0;
}
&:last-child {
border-radius: 0 0 6px 6px;
}
}
}
&:hover .thredded--post--dropdown--actions,
.thredded--post--dropdown--toggle:focus + .thredded--post--dropdown--actions {
display: block;
}
}
// Reply form
.reply-form-section {
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
margin-top: 2rem;
.reply-form-header {
padding: 1.25rem 1.5rem;
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
border-bottom: 1px solid #e5e7eb;
h3 {
font-size: 1.125rem;
font-weight: 600;
color: #111827;
margin: 0;
display: flex;
align-items: center;
.material-icons {
font-size: 1.25rem;
margin-right: 0.5rem;
color: #2196F3;
}
}
}
.reply-form-body {
padding: 1.5rem;
}
}
// Pagination styling
.topic-pagination {
display: flex;
justify-content: center;
margin: 1.5rem 0;
.pagination {
display: flex;
gap: 0.5rem;
a, span {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
border-radius: 4px;
text-decoration: none;
transition: all 0.15s ease;
}
a {
background: white;
border: 1px solid #e5e7eb;
color: #374151;
&:hover {
background: #f9fafb;
border-color: #d1d5db;
}
}
.current {
background: #2196F3;
color: white;
border: 1px solid #2196F3;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
// Locked topic notice
.locked-notice {
background: #fef3c7;
border: 1px solid #fbbf24;
border-radius: 6px;
padding: 1rem;
margin: 1.5rem 0;
display: flex;
align-items: start;
gap: 0.75rem;
.material-icons {
color: #f59e0b;
font-size: 1.25rem;
flex-shrink: 0;
}
p {
margin: 0;
color: #92400e;
font-size: 0.875rem;
line-height: 1.4;
}
}
// Dark mode support
.dark {
.topic-header-section {
background: linear-gradient(135deg, #1e40af 0%, #1e3a8a 100%);
.category-badge {
background: rgba(30, 58, 138, 0.5);
border-color: rgba(59, 130, 246, 0.5);
}
.topic-actions {
button, a {
background: #1f2937;
color: #42a5f5;
&:hover {
background: #111827;
}
&.following {
background: rgba(59, 130, 246, 0.2);
color: white;
border-color: rgba(59, 130, 246, 0.5);
}
}
}
}
.thredded--post {
background: #1f2937;
border-color: #374151;
.thredded--post--user {
background: #111827;
border-bottom-color: #374151;
.thredded--post--user-link {
color: #f3f4f6;
&:hover {
color: #42a5f5;
}
}
.thredded--post--created-at {
color: #9ca3af;
}
}
.thredded--post--content {
color: #d1d5db;
blockquote {
border-left-color: #4b5563;
color: #9ca3af;
}
code {
background: #374151;
color: #f3f4f6;
}
pre {
background: #111827;
}
}
.thredded--post--actions {
background: #111827;
border-top-color: #374151;
a {
color: #9ca3af;
&:hover {
color: #42a5f5;
}
}
}
}
.reply-form-section {
background: #1f2937;
border-color: #374151;
.reply-form-header {
background: linear-gradient(135deg, #111827 0%, #0f172a 100%);
border-bottom-color: #374151;
h3 {
color: #f3f4f6;
}
}
}
.locked-notice {
background: #451a03;
border-color: #92400e;
.material-icons {
color: #fbbf24;
}
p {
color: #fef3c7;
}
}
}
// Mobile responsiveness
@media (max-width: 768px) {
.topic-header-section {
margin-left: -1.5rem;
margin-right: -1.5rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.topic-content-area {
padding: 0 1rem;
}
.thredded--post {
.thredded--post--user,
.thredded--post--content,
.thredded--post--actions {
padding-left: 1rem;
padding-right: 1rem;
}
}
}

View File

@ -129,4 +129,17 @@ module ApplicationHelper
# Return the first sorted image, or nil if none available
combined.first
end
def unread_inbox_count
return 0 unless user_signed_in?
@unread_inbox_count ||= begin
Thredded::PrivateTopic
.for_user(current_user)
.unread(current_user)
.count
rescue
0
end
end
end

View File

@ -200,6 +200,15 @@
<span class="badge right"><%= current_user.on_premium_plan? ? 'Premium' : 'Starter' %></span>
<% end %>
</li>
<li>
<%= link_to thredded.private_topics_path, class: 'waves-effect' do %>
<i class="material-icons left purple-text">inbox</i>
Inbox
<% if unread_inbox_count > 0 %>
<span class="badge red white-text right"><%= unread_inbox_count %></span>
<% end %>
<% end %>
</li>
<li>
<%= link_to main_app.data_vault_path, class: 'waves-effect' do %>
<i class="material-icons left brown-text">lock</i>

View File

@ -303,11 +303,11 @@
<% end %>
<!-- main content -->
<main class="<%= user_signed_in? ? 'lg:ml-60' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-60 sm:ml-64': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<main class="<%= user_signed_in? ? 'lg:ml-60' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-60 sm:ml-64 lg:ml-60': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<%= yield %>
</main>
<footer class="<%= user_signed_in? ? 'lg:ml-60' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-60 sm:ml-64': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<footer class="<%= user_signed_in? ? 'lg:ml-60' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-60 sm:ml-64 lg:ml-60': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<div class="max-w-7xl mx-auto pt-24 px-4 sm:px-6 md:flex md:items-center md:justify-between lg:px-8">
<div class="flex justify-center space-x-6 md:order-2">
<%= link_to 'https://www.facebook.com/notebookai/', class: 'text-gray-400 hover:text-gray-500' do %>

View File

@ -210,11 +210,11 @@
<% end %>
<!-- main content -->
<main <% if user_signed_in? %>:class="{ 'ml-56 sm:ml-64 lg:ml-56': showSidebar }"<% end %> class="transition-all duration-300">
<main class="<%= user_signed_in? ? 'lg:ml-56' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-56 sm:ml-64 lg:ml-56': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<%= yield %>
</main>
<footer <% if user_signed_in? %>:class="{ 'ml-56 sm:ml-64 lg:ml-56': showSidebar }"<% end %> class="transition-all duration-300">
<footer class="<%= user_signed_in? ? 'lg:ml-56' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-56 sm:ml-64 lg:ml-56': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<div class="max-w-7xl mx-auto pt-24 px-4 sm:px-6 md:flex md:items-center md:justify-between lg:px-8">
<div class="flex justify-center space-x-6 md:order-2">
<%= link_to 'https://www.facebook.com/notebookai/', class: 'text-gray-400 hover:text-gray-500' do %>

View File

@ -12,6 +12,7 @@
<%= render 'layouts/favicon' %>
<%= javascript_include_tag 'application' %>
<%= javascript_include_tag 'thredded' %>
<%= javascript_pack_tag 'application' %>
<%= render 'layouts/seo' %>
<script defer type="text/javascript" src="https://js.stripe.com/v2/"></script>
@ -303,24 +304,23 @@
<% end %>
<!-- forum navigation & breadcrumbs -->
<nav class="thredded-nav bg-white border-b border-gray-200 flex <%= user_signed_in? ? 'lg:ml-56' : '' %>" <% if user_signed_in? %>:class="{ 'ml-56 sm:ml-64': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<ol role="list" class="max-w-screen-xl w-full mx-auto flex space-x-4 px-4 py-2">
<nav class="thredded-nav bg-white border-b border-gray-200 flex items-center justify-between sticky top-14 z-40 <%= user_signed_in? ? 'lg:ml-56' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-56 sm:ml-64 lg:ml-56': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<ol role="list" class="flex-1 flex space-x-4 px-4 py-2">
<%= render('thredded/redesigned/breadcrumbs') %>
</ol>
<ul class="divide-x text-sm flex items-center pr-4">
<ul class="flex items-center h-full divide-x divide-gray-200">
<li><!-- Included to always force a left-border on whatever action is left-most --></li>
<%= render 'thredded/shared/nav/moderation' %>
<%= render 'thredded/shared/nav/notification_preferences' %>
<%= render 'thredded/shared/nav/private_topics' if Thredded.private_messaging_enabled %>
</ul>
</nav>
<!-- main content -->
<main class="<%= user_signed_in? ? 'lg:ml-56' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-56 sm:ml-64': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<main class="<%= user_signed_in? ? 'lg:ml-56' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-56 sm:ml-64 lg:ml-56': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<%= yield %>
</main>
<footer class="<%= user_signed_in? ? 'lg:ml-56' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-56 sm:ml-64': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<footer class="<%= user_signed_in? ? 'lg:ml-56' : '' %> transition-all duration-300" <% if user_signed_in? %>:class="{ 'ml-56 sm:ml-64 lg:ml-56': showSidebar, 'ml-0': !showSidebar }"<% end %>>
<div class="max-w-7xl mx-auto pt-24 px-4 sm:px-6 md:flex md:items-center md:justify-between lg:px-8">
<div class="flex justify-center space-x-6 md:order-2">
<%= link_to 'https://www.facebook.com/notebookai/', class: 'text-gray-400 hover:text-gray-500' do %>

View File

@ -6,10 +6,32 @@
anything you've typed in any of those pages' fields as well. For example, searching "bald" will show you all your characters
that you've entered "bald" in for their hair style.
</p>
<%= form_tag main_app.search_path, method: :get do %>
<input type="search" name="q" value="" placeholder="search your notebook" />
<button type="submit" class="btn btn-primary blue">Search everything</button>
<%= form_tag main_app.search_path, method: :get, id: 'notebook-search-form' do %>
<input type="search" name="q" value="" placeholder="search your notebook" id="search-query" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
<div class="row">
<div class="col s6">
<button type="submit" class="btn btn-primary blue waves-effect waves-light" style="width: 100%;">
<i class="material-icons left">search</i>
Search notebook
</button>
</div>
<div class="col s6">
<button type="button" class="btn blue lighten-1 waves-effect waves-light" style="width: 100%;" onclick="searchForums()">
<i class="material-icons left">forum</i>
Search forums
</button>
</div>
</div>
<% end %>
<script>
function searchForums() {
var query = document.getElementById('search-query').value;
if (query) {
window.location.href = '/forum/?q=' + encodeURIComponent(query);
}
}
</script>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Cancel</a>

View File

@ -35,12 +35,16 @@
<input class="w-32 lg:w-48 xl:w-96 border-2 border-blue-400 text-notebook-blue focus:border-notebook-blue bg-white h-9 px-3 text-sm rounded focus:outline-none"
type="search"
name="search"
x-model="query"
@input="handleInput()"
@keydown="handleKeydown($event)"
@focus="handleFocus()"
placeholder="Search your notebook"
autocomplete="off">
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false">
<!-- Autocomplete Dropdown -->
<div x-show="showDropdown"
@ -116,6 +120,11 @@
<i class="material-icons text-sm mr-2">search</i>
Search my notebook for "<span x-text="query"></span>"
</a>
<a :href="`/forum/?q=${encodeURIComponent(query)}`"
class="flex items-center px-4 py-3 text-sm text-blue-600 hover:text-blue-800 hover:bg-gray-100 font-medium border-t border-gray-200">
<i class="material-icons text-sm mr-2">forum</i>
Search the forums for "<span x-text="query"></span>"
</a>
</div>
</div>
</div>
@ -264,6 +273,15 @@
PREMIUM
</span>
<% end %>
<%= link_to thredded.private_topics_path, class: 'flex items-center px-4 py-2 cursor-pointer group hover:bg-notebook-blue hover:text-white' do %>
<i class="material-icons text-purple-600 shrink-0 w-8 h-8 mr-2 bg-white rounded-full p-1">inbox</i>
<span class="flex-grow text-sm">Inbox</span>
<% if unread_inbox_count > 0 %>
<span class="bg-red-500 text-white rounded-full px-1.5 py-0.5 text-xs font-bold group-hover:bg-red-600">
<%= unread_inbox_count %>
</span>
<% end %>
<% end %>
<%= link_to main_app.edit_user_registration_path, class: 'flex items-center px-4 py-2 cursor-pointer group hover:bg-notebook-blue hover:text-white border-b border-gray-300' do %>
<i class="material-icons text-black shrink-0 w-8 h-8 mr-2 bg-white rounded-full p-1">settings</i>
<span class="flex-grow text-sm">Settings</span>

View File

@ -1,10 +1,60 @@
<%= view_hooks.messageboards_index.messageboard.render self, messageboard: messageboard do %>
<%= link_to messageboard.path,
class: ['thredded--messageboard hoverable',
*('thredded--messageboard--has-unread-topics' if messageboard.unread_topics?),
*('thredded--messageboard--has-unread-followed-topics' if messageboard.unread_followed_topics?)] do %>
<%= render 'thredded/messageboards/messageboard/header', messageboard: messageboard %>
<%= render 'thredded/messageboards/messageboard/description', messageboard: messageboard %>
<%= render 'thredded/messageboards/messageboard/last_updated', messageboard: messageboard %>
class: "messageboard-card messageboard-compact block" do %>
<!-- Card Header -->
<div class="messageboard-header">
<div class="flex items-start justify-between">
<h3 class="text-sm font-semibold text-gray-900 flex items-center group-hover:text-purple-600 transition-colors">
<% if messageboard.locked? %>
<i class="material-icons text-gray-500 mr-1 text-xs" title="<%= t('thredded.messageboard.form.locked_notice') %>">lock</i>
<% end %>
<%= messageboard.name %>
</h3>
<% if messageboard.unread_followed_topics? %>
<span class="messageboard-unread-badge followed inline-flex items-center">
<%= messageboard.unread_followed_topics_count %>
</span>
<% elsif messageboard.unread_topics? %>
<span class="messageboard-unread-badge new inline-flex items-center">
<%= messageboard.unread_topics_count %>
</span>
<% end %>
</div>
<!-- Description (single line) -->
<% if messageboard.description.present? %>
<p class="messageboard-description">
<%= messageboard.description %>
</p>
<% end %>
<!-- Stats -->
<div class="messageboard-stats flex items-center gap-3">
<span class="flex items-center">
<i class="material-icons mr-0.5">chat_bubble_outline</i>
<%= number_with_delimiter(messageboard.topics_count) %>
</span>
<span class="flex items-center">
<i class="material-icons mr-0.5">forum</i>
<%= number_with_delimiter(messageboard.posts_count) %>
</span>
</div>
</div>
<!-- Last Activity Footer -->
<% if messageboard.last_topic %>
<div class="messageboard-footer">
<p class="text-gray-600 truncate">
<%= time_ago(messageboard.last_topic.last_post_at) %> •
<span class="font-medium text-gray-700"><%= messageboard.last_user.thredded_display_name %></span>
</p>
</div>
<% else %>
<div class="messageboard-footer">
<p class="text-gray-500 italic">No topics yet</p>
</div>
<% end %>
<% end %>
<% end %>
<% end %>

View File

@ -0,0 +1,65 @@
<% content_for :thredded_page_title, t('thredded.messageboard.index.page_title') %>
<% content_for :thredded_page_id, 'thredded--messageboards-index' %>
<% content_for :thredded_breadcrumbs, render('thredded/shared/breadcrumbs') %>
<%= thredded_page do %>
<%= define_svg_icons('thredded/follow.svg', 'thredded/lock.svg') %>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<!-- Page Header -->
<div class="forum-page-header mb-4">
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
<i class="material-icons text-purple-600 mr-2 text-2xl">forum</i>
Community Forums
</h1>
<p class="text-sm text-gray-600 mt-1">Join discussions about worldbuilding, writing, and everything Notebook.ai</p>
</div>
<%= view_hooks.messageboards_index.container.render self, groups: @groups do %>
<%= view_hooks.messageboards_index.list.render self, groups: @groups do %>
<% @groups.each do |group| %>
<% if group.name.present? %>
<!-- Group Title -->
<h2 class="messageboard-group-title">
<%= link_to_if Thredded.show_messageboard_group_page,
group.name,
show_messageboard_group_path(group.id),
class: 'hover:text-purple-600 transition-colors' %>
</h2>
<% end %>
<!-- Messageboards Grid -->
<div class="messageboards-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mb-4">
<%= view_hooks.messageboards_index.group.render self, group: group do %>
<%= render partial: 'thredded/messageboards/messageboard',
collection: group.messageboards %>
<% end %>
</div>
<% end %>
<% end %>
<!-- Admin Actions -->
<% if policy(Thredded::Messageboard.new).create? || policy(Thredded::MessageboardGroup.new).create? %>
<div class="mt-8 pt-8 border-t border-gray-200">
<div class="flex flex-wrap gap-4">
<% if policy(Thredded::Messageboard.new).create? %>
<%= link_to new_messageboard_path,
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-colors" do %>
<i class="material-icons mr-2 text-lg">add</i>
<%= t('thredded.messageboard.create') %>
<% end %>
<% end %>
<% if policy(Thredded::MessageboardGroup.new).create? %>
<%= link_to new_messageboard_group_path,
class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-colors" do %>
<i class="material-icons mr-2 text-lg">folder</i>
<%= t('thredded.messageboard_group.create') %>
<% end %>
<% end %>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>

View File

@ -0,0 +1,38 @@
<% content_for :thredded_main_navigation do %>
<nav class="thredded--moderation-navigation">
<ul class="thredded--moderation-navigation--items">
<li class="thredded--moderation-navigation--item thredded--moderation-navigation--pending">
<%= link_to pending_moderation_path,
class: (current_page?(pending_moderation_path) ? 'active' : ''),
'aria-current': (current_page?(pending_moderation_path) ? 'page' : nil) do %>
<i class="material-icons" style="font-size: 16px; vertical-align: text-bottom; margin-right: 4px;">pending_actions</i>
<%= t('thredded.nav.moderation_pending') %>
<% end %>
</li>
<li class="thredded--moderation-navigation--item thredded--moderation-navigation--history">
<%= link_to moderation_history_path,
class: (current_page?(moderation_history_path) ? 'active' : ''),
'aria-current': (current_page?(moderation_history_path) ? 'page' : nil) do %>
<i class="material-icons" style="font-size: 16px; vertical-align: text-bottom; margin-right: 4px;">history</i>
<%= t('thredded.nav.moderation_history') %>
<% end %>
</li>
<li class="thredded--moderation-navigation--item thredded--moderation-navigation--activity">
<%= link_to moderation_activity_path,
class: (current_page?(moderation_activity_path) ? 'active' : ''),
'aria-current': (current_page?(moderation_activity_path) ? 'page' : nil) do %>
<i class="material-icons" style="font-size: 16px; vertical-align: text-bottom; margin-right: 4px;">timeline</i>
<%= t('thredded.nav.moderation_activity') %>
<% end %>
</li>
<li class="thredded--moderation-navigation--item thredded--moderation-navigation--users">
<%= link_to users_moderation_path,
class: (current_page?(users_moderation_path) ? 'active' : ''),
'aria-current': (current_page?(users_moderation_path) ? 'page' : nil) do %>
<i class="material-icons" style="font-size: 16px; vertical-align: text-bottom; margin-right: 4px;">group</i>
<%= t('thredded.nav.moderation_users') %>
<% end %>
</li>
</ul>
</nav>
<% end %>

View File

@ -0,0 +1,18 @@
<div class="thredded--post-moderation-actions divide-y divide-gray-100 dark:divide-gray-700">
<% unless post.approved? %>
<%= button_to moderate_post_path,
class: 'w-full text-left px-4 py-2 text-sm text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 hover:text-green-700 dark:hover:text-green-300 transition-colors flex items-center',
params: { id: post.to_model.id, moderation_state: 'approved' } do %>
<i class="material-icons text-base mr-2 align-middle">check_circle</i>
Approve
<% end %>
<% end %>
<% unless post.blocked? %>
<%= button_to moderate_post_path,
class: 'w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-700 dark:hover:text-red-300 transition-colors flex items-center',
params: { id: post.to_model.id, moderation_state: 'blocked' } do %>
<i class="material-icons text-base mr-2 align-middle">block</i>
Block User
<% end %>
<% end %>
</div>

View File

@ -0,0 +1,125 @@
<% content_for :thredded_page_title, t('thredded.nav.moderation') %>
<% content_for :thredded_page_id, 'thredded--moderation-activity' %>
<%= render 'nav' %>
<%= thredded_page do %>
<div class="thredded--main-section">
<!-- Activity Header -->
<div class="activity-header">
<div class="activity-title">
<i class="material-icons">timeline</i>
<h2><%= t 'thredded.recent_activity' %></h2>
</div>
</div>
<!-- Last Moderated Notice -->
<% if @last_moderated_record %>
<div class="thredded--moderated-notice">
<i class="material-icons">check_circle</i>
<div class="notice-content">
<div class="notice-title">Recently moderated</div>
<div class="notice-text">
<%= render_post_moderation_records([@last_moderated_record]) %>
</div>
</div>
<button class="notice-dismiss" onclick="this.parentElement.style.display='none'">
<i class="material-icons">close</i>
</button>
</div>
<% end %>
<!-- Activity Feed -->
<% if @posts.present? %>
<div class="activity-feed">
<div class="feed-header">
<h3>
<i class="material-icons">forum</i>
Recent Forum Activity
</h3>
</div>
<div class="feed-items">
<% @posts.each do |post| %>
<div class="feed-item">
<div class="feed-item-indicator">
<% if post.to_model.first_post_in_topic? %>
<i class="material-icons new-topic">add_comment</i>
<% else %>
<i class="material-icons reply">reply</i>
<% end %>
</div>
<div class="feed-item-content">
<div class="feed-meta">
<% if post.user %>
<%= image_tag post.avatar_url, class: 'feed-avatar' %>
<span class="feed-user"><%= user_link post.user %></span>
<% else %>
<span class="feed-user">Anonymous</span>
<% end %>
<span class="feed-action">
<% if post.to_model.first_post_in_topic? %>
started a new topic
<% else %>
replied
<% end %>
</span>
<span class="feed-location">
<% topic = post.to_model.postable %>
in <%= link_to topic.title, topic_path(topic), class: 'feed-topic-link' %>
</span>
<span class="feed-time">
<%= time_ago post.created_at %>
</span>
</div>
<div class="feed-preview">
<%= truncate(strip_tags(post.filtered_content(self)), length: 150) %>
</div>
<div class="feed-actions">
<a href="<%= post.permalink_path %>" class="feed-action-btn">
<i class="material-icons">visibility</i>
View
</a>
<% if post.can_update? %>
<%= link_to post.edit_path, class: 'feed-action-btn' do %>
<i class="material-icons">edit</i>
Edit
<% end %>
<% end %>
<% if post.can_moderate? %>
<button class="feed-action-btn moderate">
<i class="material-icons">gavel</i>
Moderate
</button>
<% end %>
</div>
</div>
<div class="feed-item-status">
<% if post.pending_moderation? %>
<span class="status-badge pending">Pending</span>
<% elsif post.blocked? %>
<span class="status-badge blocked">Blocked</span>
<% end %>
</div>
</div>
<% end %>
</div>
<%= paginate @posts %>
</div>
<% else %>
<div class="thredded--empty">
<div class="empty-icon">
<i class="material-icons">timeline</i>
</div>
<h3 class="thredded--empty--title">No recent activity</h3>
<p>There's no forum activity to display at the moment.</p>
</div>
<% end %>
</div>
<% end %>

View File

@ -0,0 +1,154 @@
<% content_for :thredded_page_title,
safe_join([t('thredded.nav.moderation'), t('thredded.nav.moderation_history')], ': ') %>
<% content_for :thredded_page_id, 'thredded--moderation-history' %>
<%= render 'nav' %>
<%= thredded_page do %>
<div class="thredded--main-section">
<!-- History Header with Filters -->
<div class="moderation-history-header">
<div class="history-title">
<i class="material-icons">history</i>
<h2>Moderation History</h2>
</div>
<div class="history-filters">
<div class="filter-group">
<label>Status:</label>
<select class="filter-select">
<option value="all">All Actions</option>
<option value="approved">Approved</option>
<option value="blocked">Blocked</option>
<option value="deleted">Deleted</option>
</select>
</div>
<div class="filter-group">
<label>Period:</label>
<select class="filter-select">
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
<option value="all">All Time</option>
</select>
</div>
<div class="filter-group">
<label>Moderator:</label>
<select class="filter-select">
<option value="all">All Moderators</option>
</select>
</div>
</div>
</div>
<% if @post_moderation_records.present? %>
<!-- Timeline Container -->
<div class="moderation-timeline">
<% @post_moderation_records.each_with_index do |record, index| %>
<%
post = record.post
post_view = Thredded::PostView.new(post, policy(post)) if post
%>
<div class="timeline-item <%= record.moderation_state %>">
<!-- Timeline Marker -->
<div class="timeline-marker">
<% if record.approved? %>
<i class="material-icons">check_circle</i>
<% elsif record.blocked? %>
<i class="material-icons">block</i>
<% else %>
<i class="material-icons">radio_button_unchecked</i>
<% end %>
</div>
<!-- Timeline Content -->
<div class="timeline-content">
<!-- Action Header -->
<div class="action-header">
<div class="action-info">
<span class="action-type <%= record.moderation_state %>">
<%= record.moderation_state.capitalize %>
</span>
<span class="action-meta">
by <%= user_link(record.moderator) %>
<span class="separator">•</span>
<%= time_ago(record.created_at) %>
</span>
</div>
<% if post %>
<div class="action-links">
<a href="<%= post_permalink_path(post) %>" class="view-post-link" target="_blank">
<i class="material-icons">open_in_new</i>
View Post
</a>
</div>
<% end %>
</div>
<!-- Post Preview -->
<div class="post-preview">
<div class="post-author">
<% if record.post_user %>
<%= image_tag record.post_user.try(:image_url, 40) || 'user-placeholder.png',
class: 'author-avatar' %>
<div class="author-info">
<div class="author-name">
<%= link_to record.post_user.thredded_display_name,
user_moderation_path(record.post_user.id) %>
</div>
<div class="post-location">
<% if post %>
in <%= link_to post.postable.title, post.postable.path %>
<% else %>
<em>Post deleted</em>
<% end %>
</div>
</div>
<% else %>
<div class="author-info">
<div class="author-name">
<%= record.post_user_name || content_tag(:em, t('thredded.null_user_name')) %>
</div>
</div>
<% end %>
</div>
<div class="post-content-preview">
<% if post && post.content != record.post_content %>
<div class="content-changed-notice">
<i class="material-icons">info</i>
Content has been modified since moderation
</div>
<% end %>
<div class="content-text">
<%= truncate(strip_tags(record.post_content_html), length: 200) %>
</div>
</div>
<% if post %>
<div class="post-actions">
<%= render 'post_moderation_actions', post: post %>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<%= paginate @post_moderation_records %>
<% else %>
<div class="thredded--empty">
<div class="empty-icon">
<i class="material-icons">history</i>
</div>
<h3 class="thredded--empty--title">No moderation history</h3>
<p>There are no moderation actions to display.</p>
</div>
<% end %>
</div>
<% end %>

View File

@ -0,0 +1,90 @@
<% content_for :thredded_page_title, t('thredded.nav.moderation') %>
<% content_for :thredded_page_id, 'thredded--pending-moderation' %>
<%= render 'nav' %>
<%= thredded_page do %>
<div class="thredded--main-section">
<!-- Moderation Stats Dashboard -->
<div class="moderation-dashboard">
<div class="stat-card">
<div class="stat-icon pending">
<i class="material-icons">pending_actions</i>
</div>
<div class="stat-value"><%= @posts.try(:total_count) || 0 %></div>
<div class="stat-label">Pending Review</div>
</div>
<div class="stat-card">
<div class="stat-icon approved">
<i class="material-icons">check_circle</i>
</div>
<div class="stat-value">--</div>
<div class="stat-label">Approved Today</div>
</div>
<div class="stat-card">
<div class="stat-icon blocked">
<i class="material-icons">block</i>
</div>
<div class="stat-value">--</div>
<div class="stat-label">Blocked Today</div>
</div>
<div class="stat-card">
<div class="stat-icon users">
<i class="material-icons">group</i>
</div>
<div class="stat-value">--</div>
<div class="stat-label">Active Users</div>
</div>
</div>
<!-- Last Moderated Notice -->
<% if @last_moderated_record %>
<div class="thredded--moderated-notice">
<i class="material-icons">check_circle</i>
<div class="notice-content">
<div class="notice-title">Successfully moderated</div>
<div class="notice-text">
<%= render_post_moderation_records([@last_moderated_record]) %>
</div>
</div>
<button class="notice-dismiss" onclick="this.parentElement.style.display='none'">
<i class="material-icons">close</i>
</button>
</div>
<% end %>
<!-- Posts List -->
<% if @posts.present? %>
<div class="moderation-posts">
<div class="moderation-posts-header">
<h3>
<i class="material-icons">forum</i>
Posts Awaiting Review
</h3>
<div class="filter-buttons">
<button class="active">All</button>
<button>Topics</button>
<button>Replies</button>
<button>Reports</button>
</div>
</div>
<%= render_posts @posts,
partial: 'thredded/moderation/post',
content_partial: 'thredded/posts/content' %>
</div>
<%= paginate @posts %>
<% else %>
<div class="thredded--empty">
<div class="empty-icon">
<i class="material-icons">inbox</i>
</div>
<h3 class="thredded--empty--title"><%= t 'thredded.moderation.pending.empty.title' %></h3>
<p><%= t 'thredded.moderation.pending.empty.content' %></p>
</div>
<% end %>
</div>
<% end %>

View File

@ -0,0 +1,139 @@
<% content_for :thredded_page_title, t('thredded.nav.moderation') %>
<% content_for :thredded_page_id, 'thredded--moderation-users' %>
<%= render 'nav' %>
<%= thredded_page do %>
<div class="thredded--main-section">
<!-- Users Header -->
<div class="users-header">
<div class="users-title">
<i class="material-icons">group</i>
<h2>User Management</h2>
</div>
<!-- Search Form -->
<div class="users-search">
<%= form_tag users_moderation_path,
method: 'get',
class: 'search-form',
'data-thredded-turboform' => true do %>
<div class="search-input-wrapper">
<i class="material-icons">search</i>
<%= text_field_tag :q, @query,
type: 'search',
class: 'search-input',
autofocus: @query.presence && !@users.presence,
placeholder: t('thredded.moderation.search_users.form_placeholder') %>
<button type="submit" class="search-button">
Search
</button>
</div>
<% end %>
</div>
</div>
<% if @users.present? %>
<% if @query.present? %>
<div class="search-results-notice">
<i class="material-icons">info</i>
<span>Showing results for "<%= @query %>"</span>
</div>
<% end %>
<!-- Users Table -->
<div class="users-table-container">
<table class="users-table">
<caption class="table-caption">
<%= page_entries_info @users %>
</caption>
<thead>
<tr>
<th class="user-column">
<i class="material-icons">person</i>
<%= t 'thredded.moderation.user.name' %>
</th>
<th class="status-column">
<i class="material-icons">security</i>
<%= t 'thredded.moderation.moderation_state.name' %>
</th>
<th class="updated-column">
<i class="material-icons">update</i>
Last Updated
</th>
<th class="actions-column">Actions</th>
</tr>
</thead>
<tbody>
<% @users.each do |user| %>
<% user_detail = user.thredded_user_detail %>
<tr class="user-row">
<td class="user-cell">
<div class="user-info">
<% if user.respond_to?(:avatar_url) %>
<%= image_tag user.avatar_url, class: 'user-avatar' %>
<% else %>
<div class="user-avatar-placeholder">
<i class="material-icons">person</i>
</div>
<% end %>
<div class="user-details">
<%= link_to user.thredded_display_name,
user_moderation_path(user.id),
class: 'user-name' %>
<span class="user-meta">
Member since <%= user.created_at.strftime('%b %Y') %>
</span>
</div>
</div>
</td>
<td class="status-cell">
<span class="moderation-badge <%= user_detail.moderation_state %>">
<% case user_detail.moderation_state.to_s %>
<% when 'approved' %>
<i class="material-icons">check_circle</i>
<% when 'blocked' %>
<i class="material-icons">block</i>
<% when 'pending_moderation' %>
<i class="material-icons">schedule</i>
<% else %>
<i class="material-icons">help</i>
<% end %>
<%= user_detail.moderation_state.to_s.humanize %>
</span>
</td>
<td class="updated-cell">
<%= time_ago user_detail.moderation_state_changed_at, default: 'Never' %>
</td>
<td class="actions-cell">
<%= link_to user_moderation_path(user.id),
class: 'action-button view' do %>
<i class="material-icons">visibility</i>
View
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= paginate @users %>
</div>
<% else %>
<div class="no-results">
<div class="no-results-icon">
<i class="material-icons">search_off</i>
</div>
<h3>No users found</h3>
<% if @query.present? %>
<p>No users match your search for "<%= @query %>"</p>
<%= link_to users_moderation_path, class: 'clear-search-btn' do %>
<i class="material-icons">clear</i>
Clear search
<% end %>
<% else %>
<p>There are no users to display.</p>
<% end %>
</div>
<% end %>
</div>
<% end %>

View File

@ -4,69 +4,137 @@
%>
<% unless blocked_post %>
<%
common_post_classes = 'p-4 text-sm'
unmuted_post_classes = 'bg-white rounded shadow' + ' ' + common_post_classes
muted_post_classes = '' + ' ' + common_post_classes
# Mute the post if it's one line that starts with ( and ends with )
muted_post = post.present? && post.to_model.content.split("\n").reject(&:empty?).all? { |paragraph| paragraph.strip.start_with?('(') && paragraph.strip.end_with?(')') }
%>
<%= render 'thredded/posts_common/before_first_unread_post', post: post if post.first_unread_in_page? %>
<%= content_tag :article, id: dom_id(post), class: "thredded--post thredded--#{post.read_state}--post" do %>
<div class="flex space-x-4">
<div>
<span class="inline-block relative">
<% if post.user %>
<%= image_tag post.user.image_url(120), class: 'h-16 w-16 rounded-full border border-gray-200' %>
<%= content_tag :article,
id: dom_id(post),
class: ["thredded--post", "thredded--#{post.read_state}--post", "mb-6"] do %>
<!-- Two-column layout: Avatar column + Content column -->
<div class="flex gap-4">
<!-- Left Column: User Avatar & Info -->
<div class="flex-shrink-0 w-12 sm:w-16">
<% if post.user %>
<div class="relative">
<!-- User Avatar -->
<%= image_tag post.user.image_url(120),
class: 'h-12 w-12 sm:h-16 sm:w-16 rounded-full border-2 border-gray-200 dark:border-gray-600 shadow-sm' %>
<!-- Favorite Page Type Badge -->
<% if post.user.favorite_page_type? %>
<%= link_to main_app.send("#{post.user.favorite_page_type.downcase.pluralize}_user_path", { id: post.user.id }) do %>
<span class="absolute bottom-2 right-2 transform translate-y-1/2 translate-x-1/2 block border-2 border-white rounded-full">
<span class="block h-6 w-6 rounded-full text-center <%= content_class_from_name(post.user.favorite_page_type).color %>">
<i class="material-icons text-sm text-white"><%= content_class_from_name(post.user.favorite_page_type).icon %></i>
<span class="absolute -bottom-1 -right-1 block">
<span class="block h-5 w-5 rounded-full text-center <%= content_class_from_name(post.user.favorite_page_type).color %> border-2 border-white dark:border-gray-800 shadow-sm">
<i class="material-icons text-xs text-white"><%= content_class_from_name(post.user.favorite_page_type).icon %></i>
</span>
</span>
<% end %>
<% end %>
</div>
<!-- Role Badges under avatar -->
<% if post.user&.forum_moderator? %>
<div class="mt-2 flex justify-center">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200">
<i class="material-icons text-xs mr-1">verified_user</i>
Mod
</span>
</div>
<% end %>
</span>
</div>
<div class="bg-white flex-1 border border-gray-200 rounded divide-y">
<div class="px-2 py-1 bg-gray-50 rounded-t flex">
<div class="flex-1">
<h2 class="thredded--post--user text-sm">
<%= user_link post.user %>
</h2>
<% if post.user&.forum_moderator? %>
<i class="material-icons float-left text-notebook-blue mr-1">
health_and_safety
</i>
<% end %>
</div>
<div class="h-6">
<span class="relative -top-0.5">
<a href="<%= post.permalink_path %>" rel="nofollow" class="thredded--link thredded--post--created-at text-xs">
<%= time_ago post.created_at %>
</a>
</span>
<span class="relative top-0.5">
<%= render 'thredded/posts_common/actions', post: post, actions: local_assigns[:actions] %>
</span>
</div>
</div>
<div class="p-4">
<%= ForumReplacementService.replace_for(content, current_user) || render('thredded/posts/content', post: post) %>
</div>
<% if post.pending_moderation? && !Thredded.content_visible_while_pending_moderation %>
<p class="thredded--alert thredded--alert-warning"><%= t 'thredded.posts.pending_moderation_notice' %></p>
<% elsif post.blocked? && post.can_moderate? %>
<p class="thredded--alert thredded--alert-danger">
<%= render 'thredded/shared/content_moderation_blocked_state', moderation_record: post.last_moderation_record %>
</p>
<% end %>
</div>
<!-- Right Column: Message Content (Speech Bubble) -->
<div class="flex-1 min-w-0">
<!-- Speech Bubble Container -->
<div class="relative">
<!-- Speech Bubble Arrow -->
<div class="absolute left-0 top-4 transform -translate-x-2">
<div class="w-0 h-0 border-t-8 border-b-8 border-r-8 border-t-transparent border-b-transparent border-r-white dark:border-r-gray-800"></div>
</div>
<!-- Message Card -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 ml-2">
<!-- Message Content -->
<div class="px-4 py-3">
<!-- User Name and Badge at top of message -->
<div class="flex items-center gap-2 mb-2">
<span class="font-medium <%= User.text_color %>">
<%= user_link post.user %>
</span>
<% if post.user.forums_badge_text.present? %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">
<%= post.user.forums_badge_text %>
</span>
<% end %>
</div>
<div class="prose prose-sm max-w-none text-gray-900 dark:text-gray-100">
<%= ForumReplacementService.replace_for(content, current_user) || render('thredded/posts/content', post: post) %>
</div>
<!-- Moderation Notices -->
<% if post.pending_moderation? && !Thredded.content_visible_while_pending_moderation %>
<div class="mt-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-lg p-3">
<div class="flex">
<div class="flex-shrink-0">
<i class="material-icons text-yellow-400 text-base">pending</i>
</div>
<div class="ml-3">
<p class="text-sm text-yellow-800 dark:text-yellow-200">
<%= t 'thredded.posts.pending_moderation_notice' %>
</p>
</div>
</div>
</div>
<% elsif post.blocked? && post.can_moderate? %>
<div class="mt-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 rounded-lg p-3">
<div class="flex">
<div class="flex-shrink-0">
<i class="material-icons text-red-400 text-base">block</i>
</div>
<div class="ml-3">
<p class="text-sm text-red-800 dark:text-red-200">
<%= render 'thredded/shared/content_moderation_blocked_state', moderation_record: post.last_moderation_record %>
</p>
</div>
</div>
</div>
<% end %>
</div>
<!-- Message Footer -->
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-900/50 border-t border-gray-100 dark:border-gray-700 rounded-b-lg">
<div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-3">
<!-- Timestamp -->
<a href="<%= post.permalink_path %>" rel="nofollow" class="hover:text-purple-600 dark:hover:text-purple-400 transition-colors">
<%= time_ago post.created_at %>
</a>
<!-- First Post Stats -->
<% if defined?(post_counter) && post_counter == 0 %>
<span class="flex items-center">
<i class="material-icons text-xs mr-1">visibility</i>
<%= number_with_delimiter(post.topic.views_count) %> views
</span>
<span class="flex items-center">
<i class="material-icons text-xs mr-1">forum</i>
<%= post.topic.posts_count - 1 %> <%= 'reply'.pluralize(post.topic.posts_count - 1) %>
</span>
<% end %>
</div>
<!-- Post Actions -->
<div class="flex items-center">
<%= render 'thredded/posts_common/actions', post: post, actions: local_assigns[:actions] %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<% end %>
<% end %>
<% end %>

View File

@ -0,0 +1,55 @@
<% actions ||= {} %>
<% actions_html = capture do %>
<%= view_hooks.post_common.actions.render self, post: post do %>
<%# Primary actions %>
<div class="py-1">
<% if actions[:quote] && post.can_reply? %>
<%= render 'thredded/posts_common/actions/quote', post: post %>
<% end %>
<% if post.can_update? %>
<%= render 'thredded/posts_common/actions/edit', post: post %>
<% end %>
<% if post.can_destroy? %>
<%= render 'thredded/posts_common/actions/delete', post: post %>
<% end %>
<% if post.read_state %>
<%= view_hooks.post_common.mark_as_unread.render self, post: post do %>
<%= render 'thredded/posts_common/actions/mark_as_unread', post: post %>
<% end %>
<% end %>
</div>
<%# Moderation/dangerous actions %>
<% if post.can_report? || (user_signed_in? && current_user.forum_moderator? && post.instance_variable_get(:@post).is_a?(Thredded::Post)) %>
<div class="py-1 border-t border-gray-100 dark:border-gray-700">
<% if post.can_report? %>
<%= render 'thredded/posts_common/actions/report', post: post %>
<% end %>
<% if user_signed_in? && current_user.forum_moderator? && post.instance_variable_get(:@post).is_a?(Thredded::Post) %>
<%= render 'thredded/moderation/post_moderation_actions', post: post %>
<% end %>
</div>
<% end %>
<% end %>
<% end %>
<%- if actions_html.present? %>
<div class='relative inline-block text-left' x-data="{ open: false }" @click.outside="open = false">
<button @click="open = !open" type="button" class="flex items-center text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100 p-2 transition-colors">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z" />
</svg>
</button>
<div x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class='absolute right-0 z-10 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black dark:ring-gray-700 ring-opacity-5 focus:outline-none'
style="display: none;">
<%= actions_html %>
</div>
</div>
<% end %>

View File

@ -0,0 +1,22 @@
<div class="thredded--post--content">
<style>
.thredded--post--content a {
color: rgb(37 99 235); /* blue-600 */
text-decoration: underline;
text-decoration-color: rgb(37 99 235 / 0.3);
}
.thredded--post--content a:hover {
color: rgb(29 78 216); /* blue-700 */
text-decoration-color: rgb(29 78 216 / 0.5);
}
.dark .thredded--post--content a {
color: rgb(96 165 250); /* blue-400 */
text-decoration-color: rgb(96 165 250 / 0.3);
}
.dark .thredded--post--content a:hover {
color: rgb(147 197 253); /* blue-300 */
text-decoration-color: rgb(147 197 253 / 0.5);
}
</style>
<%= post.filtered_content(self, **(local_assigns[:options] || {})) %>
</div>

View File

@ -0,0 +1,33 @@
<%# locals: post, content_label, button_text, button_submitting_text. %>
<%= form_for post,
url: post.submit_path,
as: :post,
html: {
class: 'thredded--form thredded--post-form',
'data-thredded-post-form' => true,
'data-autocomplete-url' => autocomplete_users_path,
'data-autocomplete-min-length' => Thredded.autocomplete_min_length,
'data-thredded-submit-hotkey' => true,
} do |form| %>
<div class="space-y-4">
<div>
<%= render 'thredded/posts_common/form/content_field',
form: form, content_label: content_label %>
</div>
<%= render 'thredded/posts_common/form/preview_area',
form: form, preview_url: post.preview_path %>
<div class="flex items-center justify-between">
<% button_submitting_text ||=
post.persisted? ? t('thredded.form.update_btn_submitting') : t('thredded.form.create_btn_submitting') %>
<button type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800 transition-colors"
data-disable-with="<%= button_submitting_text %>">
<%= button_text %>
</button>
<div class="text-xs text-gray-500 dark:text-gray-400">
<kbd class="px-2 py-1 text-xs font-semibold text-gray-800 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg">Ctrl</kbd> + <kbd class="px-2 py-1 text-xs font-semibold text-gray-800 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg">Enter</kbd> to submit
</div>
</div>
</div>
<% end %>

View File

@ -0,0 +1,8 @@
<%= link_to post.destroy_path,
method: :delete,
data: { confirm: t('thredded.posts.delete_confirm') },
class: 'thredded--post--delete thredded--post--dropdown--actions--item block px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-700 dark:hover:text-red-300 transition-colors',
rel: 'nofollow' do %>
<i class="material-icons text-base mr-2 align-middle">delete</i>
Delete
<% end %>

View File

@ -0,0 +1,6 @@
<%= link_to post.edit_path,
class: 'thredded--post--edit thredded--post--dropdown--actions--item block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100 transition-colors',
rel: 'nofollow' do %>
<i class="material-icons text-base mr-2 align-middle">edit</i>
Edit
<% end %>

View File

@ -0,0 +1,6 @@
<%= button_to post.mark_unread_path,
method: :post,
class: 'thredded--post--mark-as-unread thredded--post--dropdown--actions--item w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100 transition-colors flex items-center' do %>
<i class="material-icons text-base mr-2 align-middle">mark_email_unread</i>
Mark as Unread
<% end %>

View File

@ -0,0 +1,7 @@
<%= link_to post.quote_url_params,
'data-thredded-quote-post' => post.quote_path,
class: 'thredded--post--quote thredded--post--dropdown--actions--item block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100 transition-colors',
rel: 'nofollow' do %>
<i class="material-icons text-base mr-2 align-middle">format_quote</i>
Quote
<% end %>

View File

@ -0,0 +1,7 @@
<%= link_to post.report_path,
method: :post,
class: 'thredded--post--report thredded--post--dropdown--actions--item block px-4 py-2 text-sm text-orange-600 dark:text-orange-400 hover:bg-orange-50 dark:hover:bg-orange-900/20 hover:text-orange-700 dark:hover:text-orange-300 transition-colors',
rel: 'nofollow' do %>
<i class="material-icons text-base mr-2 align-middle">flag</i>
Report
<% end %>

View File

@ -1,13 +1,22 @@
<li>
<%= form.label :content, content_label %>
<div>
<%= form.label :content, content_label, class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2" %>
<%= view_hooks.post_form.content_text_area.render self, form: form, content_label: content_label do %>
<%= render 'thredded/posts_common/form/before_content', form: form %>
<%= form.text_area :content, {rows: 5, required: true, autofocus: !!params[:autofocus_new_post_content], class: 'js-can-mention-pages'} %>
<%= render 'thredded/posts_common/form/after_content', form: form %>
<%= render 'thredded/posts_common/form/before_content', form: form %>
<%= form.text_area :content, {
rows: 5,
required: true,
autofocus: !!params[:autofocus_new_post_content],
class: "js-can-mention-pages shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md p-3 resize-vertical",
placeholder: "Write your message here..."
} %>
<%= render 'thredded/posts_common/form/after_content', form: form %>
<% end %>
<div>
<%= link_to 'https://www.markdownguide.org/basic-syntax/', target: '_blank' do %>
<span class="hoverable badge new blue white-text" data-badge-caption="formatting help"></span>
<div class="mt-2">
<%= link_to 'https://www.markdownguide.org/basic-syntax/',
target: '_blank',
class: 'inline-flex items-center text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300' do %>
<i class="material-icons text-sm mr-1">help_outline</i>
Markdown formatting help
<% end %>
</div>
</li>
</div>

View File

@ -0,0 +1,23 @@
<%= view_hooks.post_form.preview_area.render self, form: form, preview_url: preview_url do %>
<div>
<noscript>
<button type="submit" class="inline-flex items-center px-3 py-1 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
formtarget="_blank" formaction="<%= preview_url %>">
<%= t 'thredded.form.preview' %>
</button>
</noscript>
<div class="thredded--preview-area border border-gray-200 dark:border-gray-600 rounded-md p-4 bg-gray-50 dark:bg-gray-800"
data-thredded-preview-area data-thredded-preview-url="<%= preview_url %>"
style="display: none;">
<h4 class="thredded--preview-area--title text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
<%= t 'thredded.form.preview' %>
</h4>
<div class="thredded--preview-area--post thredded--post--content prose prose-sm max-w-none
prose-a:text-blue-600 prose-a:underline prose-a:decoration-blue-600/30 hover:prose-a:text-blue-700 hover:prose-a:decoration-blue-700/50
dark:prose-invert dark:prose-a:text-blue-400 dark:hover:prose-a:text-blue-300
dark:text-gray-200"
data-thredded-preview-area-post></div>
</div>
</div>
<% end %>

View File

@ -0,0 +1,78 @@
<%= form_for private_topic,
url: private_topic.submit_path,
html: {
class: "thredded--form thredded--new-private-topic-form #{local_assigns[:css_class]}",
'data-thredded-topic-form' => true,
'data-autocomplete-url' => autocomplete_users_path,
'data-autocomplete-min-length' => Thredded.autocomplete_min_length,
'data-thredded-submit-hotkey' => true,
} do |form| %>
<ul class="thredded--form-list on-top">
<li class="title">
<%= form.label :title, t('thredded.private_topics.form.title_label') %>
<%= form.text_field :title, placeholder: placeholder, required: true, autocomplete: 'off' %>
<%= render 'thredded/shared/field_errors', messages: form.object.errors[:title] %>
</li>
<li>
<%= form.label :user_names, t('thredded.private_topics.form.users_label') %>
<%= form.text_area :user_names,
required: true,
placeholder: t('thredded.private_topics.form.users_placeholder'),
'data-thredded-users-select' => true,
'data-autocomplete-url' => autocomplete_users_path,
'data-autocomplete-min-length' => Thredded.autocomplete_min_length,
rows: 1 %>
<div class="recipients-helper">
<i class="material-icons">info</i>
Start typing to search for users
</div>
<%= render 'thredded/shared/field_errors', messages: form.object.errors[:user_names] %>
</li>
<%= render 'thredded/posts_common/form/content',
form: form,
content_label: t('thredded.private_topics.form.content_label'),
preview_url: private_topic.preview_path %>
<li>
<div class="compose-actions">
<div class="left-actions">
<a href="<%= private_topics_path %>" class="btn-cancel">
<i class="material-icons">arrow_back</i>
Cancel
</a>
</div>
<div class="right-actions">
<button type="button" class="btn-preview" data-thredded-preview-toggle>
<i class="material-icons">visibility</i>
Preview
</button>
<button type="submit" class="thredded--form--submit"
data-disable-with="<%= t 'thredded.private_topics.form.create_btn_submitting' %>">
<i class="material-icons">send</i>
<%= t('thredded.private_topics.form.create_btn') %>
</button>
</div>
</div>
</li>
</ul>
<% end %>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle preview toggle
const previewBtn = document.querySelector('[data-thredded-preview-toggle]');
const previewArea = document.querySelector('.thredded--preview-area');
if (previewBtn && previewArea) {
previewBtn.addEventListener('click', function() {
previewArea.style.display = previewArea.style.display === 'none' ? 'block' : 'none';
previewBtn.classList.toggle('active');
});
}
});
</script>

View File

@ -0,0 +1,8 @@
<div class="thredded--empty">
<h3 class="thredded--empty--title">No messages yet</h3>
<p>Start a private conversation with other members</p>
<a class="thredded--button" href="<%= new_private_topic_path %>">
<i class="material-icons">edit</i>
<%= t 'thredded.private_topics.no_private_topics.create_btn' %>
</a>
</div>

View File

@ -0,0 +1,49 @@
<%= content_tag :article,
id: dom_id(private_topic),
class: ['thredded--topics--topic', *topic_css_classes(private_topic)] do %>
<!-- Participants Avatars -->
<div class="message-participants">
<% participants = [private_topic.last_user, *(private_topic.users - [private_topic.last_user])].compact.uniq.first(3) %>
<% participants.each do |user| %>
<div class="participant-avatar" title="<%= user.thredded_display_name %>">
<% if user.respond_to?(:avatar_url) && user.avatar_url.present? %>
<%= image_tag user.avatar_url, alt: user.thredded_display_name %>
<% else %>
<span class="initials"><%= user.thredded_display_name.first(2) %></span>
<% end %>
</div>
<% end %>
<% if private_topic.users.count > 3 %>
<span class="more-participants">+<%= private_topic.users.count - 3 %></span>
<% end %>
</div>
<!-- Message Content -->
<div class="message-content">
<h1 class="thredded--topics--title">
<%= link_to private_topic.title, private_topic.path %>
</h1>
<% if private_topic.last_post %>
<div class="message-preview">
<%= truncate(private_topic.last_post.content.to_s.gsub(/<[^>]*>/, ''), length: 100) %>
</div>
<% end %>
</div>
<!-- Posts Count Badge -->
<div class="thredded--topics--posts-count">
<%= private_topic.posts_count %>
</div>
<!-- Timestamp and Metadata -->
<cite class="thredded--topics--updated-by">
<%= time_ago private_topic.last_post_at %>
<div class="thredded--topics--participants">
<% if private_topic.last_user %>
<%= link_to private_topic.last_user.thredded_display_name, user_path(private_topic.last_user) %>
<% end %>
</div>
</cite>
<% end %>

View File

@ -0,0 +1,75 @@
<% content_for :thredded_page_title, t('thredded.nav.private_topics') %>
<% content_for :thredded_page_id, 'thredded--private-topics-index' %>
<% content_for :thredded_breadcrumbs, render('thredded/private_topics/breadcrumbs') %>
<%= thredded_page do %>
<div class="thredded--private-topics">
<!-- Page Header -->
<div class="private-messages-header">
<div class="header-content">
<h1>
<i class="material-icons">mail</i>
Private Messages
</h1>
<div class="header-actions">
<a href="<%= new_private_topic_path %>" class="btn-compose">
<i class="material-icons">edit</i>
Compose
</a>
</div>
</div>
<% if @private_topics.to_a.any? %>
<div class="inbox-stats">
<div class="stat">
<i class="material-icons">inbox</i>
<span><strong><%= @private_topics.to_a.count %></strong> conversations</span>
</div>
<% unread_count = unread_private_topics_count %>
<% if unread_count > 0 %>
<div class="stat">
<i class="material-icons">mark_email_unread</i>
<span><strong><%= unread_count %></strong> unread</span>
</div>
<% end %>
</div>
<% end %>
</div>
<!-- Messages List or Empty State -->
<% if @private_topics.to_a.empty? %>
<%= render 'thredded/private_topics/no_private_topics' %>
<% else %>
<div class="messages-list">
<%= content_tag :section,
'data-thredded-topics' => 'private',
'data-thredded-topic-posts-per-page' => Thredded.posts_per_page do %>
<% if @new_private_topic %>
<%= render 'thredded/private_topics/form',
private_topic: @new_private_topic,
css_class: 'thredded--is-compact',
placeholder: t('thredded.private_topics.form.title_placeholder_start') %>
<% end %>
<%= render @private_topics %>
<% end %>
</div>
<% unless unread_private_topics_count.zero? %>
<%= link_to mark_all_private_topics_read_path,
class: 'thredded--button-wide',
method: :put do %>
<i class="material-icons mr-2">done_all</i>
<%= t('thredded.nav.mark_all_read') %>
<% end %>
<% end %>
<% end %>
<!-- Pagination -->
<footer class="thredded--pagination-bottom">
<%= paginate @private_topics %>
</footer>
</div>
<% end %>

View File

@ -0,0 +1,25 @@
<% content_for :thredded_page_title, t('thredded.private_topics.create') %>
<% content_for :thredded_page_id, 'thredded--new-private-topic' %>
<% content_for :thredded_breadcrumbs, render('thredded/private_topics/breadcrumbs') %>
<%= thredded_page do %>
<div class="compose-container">
<div class="compose-wrapper">
<!-- Compose Header -->
<div class="compose-header">
<h1>
<i class="material-icons">mail_outline</i>
Compose Private Message
</h1>
<p class="compose-subtitle">Start a private conversation with other members</p>
</div>
<!-- Compose Form -->
<section class="thredded--main-section">
<%= render 'thredded/private_topics/form',
private_topic: @private_topic,
placeholder: t('thredded.private_topics.form.title_placeholder_new') if @private_topic %>
</section>
</div>
</div>
<% end %>

View File

@ -1,6 +1,6 @@
<li class="flex">
<div class="flex items-center">
<%= link_to main_app.thredded_path, class: 'text-gray-400 hover:text-blue-500' do %>
<%= link_to main_app.thredded_path, class: 'px-2 py-1 -mx-2 -my-1 text-gray-400 hover:text-blue-500 rounded transition-colors' do %>
<i class="material-icons flex-shrink-0 relative top-1">forum</i>
<span class="sr-only">Home</span>
<% end %>
@ -13,7 +13,7 @@
<svg class="flex-shrink-0 w-6 h-full text-gray-200" viewBox="0 0 24 44" preserveAspectRatio="none" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M.293 0l22 22-22 22h1.414l22-22-22-22H.293z" />
</svg>
<%= link_to messageboard.name, messageboard_topics_path(messageboard), class: 'ml-4 text-sm font-medium text-gray-500 hover:text-blue-500' %>
<%= link_to messageboard.name, messageboard_topics_path(messageboard), class: 'ml-4 px-2 py-1 -mx-2 -my-1 text-sm font-medium text-gray-500 hover:text-blue-500 rounded transition-colors' %>
</div>
</li>
<% else %>
@ -22,7 +22,28 @@
<svg class="flex-shrink-0 w-6 h-full text-gray-200" viewBox="0 0 24 44" preserveAspectRatio="none" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M.293 0l22 22-22 22h1.414l22-22-22-22H.293z" />
</svg>
<%= link_to 'Notebook.ai Forums', root_path, class: 'ml-4 text-sm font-medium text-gray-500 hover:text-blue-500' %>
<% if controller_name == 'private_topics' || controller_name == 'private_posts' %>
<%= link_to 'Inbox', thredded.private_topics_path, class: 'ml-4 px-2 py-1 -mx-2 -my-1 text-sm font-medium text-gray-500 hover:text-blue-500 rounded transition-colors' %>
<% else %>
<%= link_to 'Notebook.ai Forums', root_path, class: 'ml-4 px-2 py-1 -mx-2 -my-1 text-sm font-medium text-gray-500 hover:text-blue-500 rounded transition-colors' %>
<% end %>
</div>
</li>
<% end %>
<% # Check for topic on the page - multiple ways to find it %>
<% topic = nil %>
<% topic = @topic if defined?(@topic) && @topic.try(:persisted?) %>
<% topic = @posts.topic if !topic && defined?(@posts) && @posts.respond_to?(:topic) && @posts.topic.try(:persisted?) %>
<% topic = local_assigns[:topic] if !topic && defined?(local_assigns) && local_assigns[:topic].try(:persisted?) %>
<% if topic %>
<li class="flex">
<div class="flex items-center">
<svg class="flex-shrink-0 w-6 h-full text-gray-200" viewBox="0 0 24 44" preserveAspectRatio="none" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M.293 0l22 22-22 22h1.414l22-22-22-22H.293z" />
</svg>
<span class="ml-4 px-2 py-1 -mx-2 -my-1 text-sm font-medium text-gray-700 truncate max-w-xs"><%= topic.title %></span>
</div>
</li>
<% end %>

View File

@ -0,0 +1,35 @@
<% if (users = active_users).present? %>
<aside
data-thredded-currently-online
class="thredded--currently-online"
x-data="{
expanded: false,
timer: null,
userCount: <%= users.count %>
}"
@mouseenter="
clearTimeout(timer);
expanded = true;
$el.querySelector('.thredded--currently-online--users').classList.add('expanded');
$el.querySelector('.thredded--currently-online--title').classList.remove('collapsed');
"
@mouseleave="
timer = setTimeout(() => {
expanded = false;
$el.querySelector('.thredded--currently-online--users').classList.remove('expanded');
$el.querySelector('.thredded--currently-online--title').classList.add('collapsed');
}, 300);
"
@click="
if (!expanded) {
clearTimeout(timer);
expanded = true;
$el.querySelector('.thredded--currently-online--users').classList.add('expanded');
$el.querySelector('.thredded--currently-online--title').classList.remove('collapsed');
}
"
>
<%= render 'thredded/shared/currently_online/header', users: users %>
<%= render 'thredded/shared/currently_online/user_list', users: users %>
</aside>
<% end %>

View File

@ -0,0 +1,5 @@
<header>
<h3 class="thredded--currently-online--title collapsed" data-count="<%= users.count %>">
<%= t "thredded.users.currently_online" %>
</h3>
</header>

View File

@ -1,11 +1,18 @@
<% if thredded_moderator? %>
<% current = current_page_moderation? %>
<li class="px-4 text-center m-0 thredded--user-navigation--item thredded--user-navigation--moderation<%= ' thredded--is-current' if current %>">
<%= link_to current ? nav_back_path : pending_moderation_path, rel: 'nofollow' do %>
<span class="thredded--nav-text"><%= t 'thredded.nav.moderation' %></span>
<li class="thredded--user-navigation--item thredded--user-navigation--moderation<%= ' thredded--is-current' if current %>" style="display: flex; align-items: center; height: 100%; margin: 0;">
<%= link_to current ? nav_back_path : pending_moderation_path,
rel: 'nofollow',
style: 'display: flex; align-items: center; height: 100%; padding: 0.75rem 1rem; text-decoration: none; color: #374151; transition: background-color 0.15s;',
onmouseover: "this.style.backgroundColor='#f9fafb'",
onmouseout: "this.style.backgroundColor='transparent'",
title: t('thredded.nav.moderation') do %>
<span style="font-size: 14px;"><%= t 'thredded.nav.moderation' %></span>
<% if posts_pending_moderation_count > 0 %>
<span class="thredded--user-navigation--moderation--pending-count"><%= posts_pending_moderation_count %></span>
<span style="margin-left: 0.25rem; display: inline-flex; align-items: center; padding: 0.125rem 0.375rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 500; background-color: #fee2e2; color: #991b1b;">
<%= posts_pending_moderation_count %>
</span>
<% end %>
<% end %>
</li>
<% end %>
<% end %>

View File

@ -1,8 +1,16 @@
<% if false %>
<% current = current_page_preferences? %>
<li class="px-4 m-0 thredded--user-navigation--settings thredded--user-navigation--item<%= ' thredded--is-current' if current %>">
<%= link_to current ? nav_back_path(messageboard) : edit_preferences_path(messageboard), rel: 'nofollow' do %>
<span class="thredded--nav-text"><%= t('thredded.nav.settings') %></span>
<% end %>
</li>
<%# Notification preferences are disabled for now since they require messageboard context %>
<%# which isn't available on all forum pages (like the messageboards index) %>
<% if false %>
<% if user_signed_in? && @messageboard %>
<% current = current_page_preferences? %>
<li class="thredded--user-navigation--item thredded--user-navigation--settings<%= ' thredded--is-current' if current %> flex items-center h-full">
<%= link_to current ? nav_back_path(@messageboard) : edit_preferences_path(@messageboard),
rel: 'nofollow',
class: 'flex items-center h-full px-4 py-3 text-center hover:bg-gray-50 transition-colors',
title: t('thredded.nav.settings') do %>
<i class="material-icons text-gray-600 text-base">notifications</i>
<span class="sr-only"><%= t('thredded.nav.settings') %></span>
<% end %>
</li>
<% end %>
<% end %>

View File

@ -1,12 +0,0 @@
<% current = current_page_private_topics? %>
<li class="px-4 thredded--user-navigation--item thredded--user-navigation--private-topics<%= ' thredded--is-current' if current %>">
<%= link_to current ? nav_back_path : private_topics_path, rel: 'nofollow' do %>
<%# inline_svg_tag 'thredded/private-messages.svg',
class: 'thredded--icon',
title: t('thredded.nav.private_topics') %>
<span class="thredded--nav-text"><%= t('thredded.nav.private_topics') %></span>
<% if unread_private_topics_count > 0 -%>
<span class="thredded--user-navigation--private-topics--unread"><%= unread_private_topics_count %></span>
<% end -%>
<% end -%>
</li>

View File

@ -0,0 +1,160 @@
<div x-data="{
expanded: false,
hasContent: false,
showPreview: false,
titleValue: '',
contentValue: '',
previewInitialized: false,
toggleExpand() {
if (!this.expanded) {
this.expanded = true;
this.$nextTick(() => {
this.$refs.titleInput.focus();
});
}
},
collapse() {
if (!this.hasContent) {
this.expanded = false;
this.showPreview = false;
}
},
checkContent() {
this.hasContent = (this.titleValue.trim() !== '' || this.contentValue.trim() !== '');
},
togglePreview() {
this.showPreview = !this.showPreview;
if (this.showPreview && !this.previewInitialized) {
this.$nextTick(() => {
const form = this.$refs.topicForm;
const textarea = form.querySelector('textarea#topic_content');
if (window.ThreddedPreviewArea && textarea) {
new window.ThreddedPreviewArea(form, textarea);
this.previewInitialized = true;
textarea.dispatchEvent(new Event('input'));
}
});
} else if (this.showPreview && this.previewInitialized) {
this.$nextTick(() => {
const textarea = this.$refs.topicForm.querySelector('textarea#topic_content');
if (textarea) {
textarea.dispatchEvent(new Event('input'));
}
});
}
}
}"
x-on:click.away="collapse()"
x-on:keydown.escape.window="collapse()"
class="thredded--new-topic-form-wrapper">
<!-- Collapsed State -->
<div x-show="!expanded"
x-transition:enter="form-expand-enter form-expand-enter-active"
x-transition:leave="form-expand-leave form-expand-leave-active"
class="new-topic-collapsed">
<div @click="toggleExpand()" class="collapsed-input" tabindex="0">
<span class="collapsed-placeholder">Start a new discussion...</span>
</div>
</div>
<!-- Expanded State -->
<div x-show="expanded"
x-cloak
x-transition:enter="form-expand-enter form-expand-enter-active"
x-transition:leave="form-expand-leave form-expand-leave-active"
class="new-topic-expanded">
<div class="form-header">
<div class="form-title">
<i class="material-icons">edit</i>
New Discussion
</div>
</div>
<%= form_for topic,
url: topic.submit_path,
html: {
class: "thredded--form",
'x-ref' => 'topicForm',
'data-thredded-topic-form' => true,
'data-autocomplete-url' => autocomplete_users_path,
'data-autocomplete-min-length' => Thredded.autocomplete_min_length,
'data-thredded-submit-hotkey' => true,
} do |form| %>
<%= hidden_field_tag("next_page", params[:next_page]) %>
<div class="form-content">
<ul class="thredded--form-list">
<li class="title">
<%= form.label :title, t('thredded.topics.form.title_label') %>
<%= form.text_field :title,
placeholder: placeholder,
required: true,
autocomplete: 'off',
'x-ref': 'titleInput',
'x-model': 'titleValue',
'@input': 'checkContent()' %>
<%= render 'thredded/shared/field_errors', messages: form.object.errors[:title] %>
</li>
<li>
<%= form.label :content, t('thredded.topics.form.content_label') %>
<%= form.text_area :content,
rows: 5,
required: true,
placeholder: "Share your thoughts...",
'x-model': 'contentValue',
'@input': 'checkContent()' %>
<%= render 'thredded/shared/field_errors', messages: form.object.errors[:content] %>
</li>
<% if form.object.category_options.any? %>
<li class="category">
<%= form.label :category_ids, "Categories (optional)" %>
<%= form.select :category_ids,
form.object.category_options,
{},
{ multiple: true, 'data-placeholder' => t('thredded.topics.form.categories_placeholder') } %>
</li>
<% end %>
<!-- Preview Area -->
<li x-show="showPreview" x-cloak>
<div class="thredded--preview-area"
data-thredded-preview-area
data-thredded-preview-url="<%= topic.preview_path %>">
<div class="thredded--preview-area--post thredded--post--content"
data-thredded-preview-area-post></div>
</div>
</li>
<%= render 'thredded/topics/topic_form_admin_options', form: form if defined?(topic_form_admin_options) %>
</ul>
</div>
<div class="form-actions">
<div class="left-actions">
<button type="button"
@click="togglePreview()"
:class="{ 'active': showPreview }"
class="btn-preview">
<i class="material-icons">visibility</i>
<span x-text="showPreview ? 'Hide Preview' : 'Preview'"></span>
</button>
</div>
<div class="right-actions">
<button type="button"
@click="expanded = false; titleValue = ''; contentValue = ''; hasContent = false; showPreview = false; previewInitialized = false"
class="btn-cancel">
Cancel
</button>
<%= form.submit t('thredded.topics.form.create_btn'),
class: 'thredded--form--submit',
':disabled': '!hasContent' %>
</div>
</div>
<% end %>
</div>
</div>

View File

@ -1,92 +1,73 @@
<header class="thredded--topic-header">
<%= view_hooks.topic_page.title.render self, topic: topic do %>
<h1 class="thredded--topic-header--title">
<%= topic.title %>
</h1>
<div class="thredded--topic-header--started-by">
<div class="bg-blue-100 text-black p-2 my-2 ml-1 text-sm">
<i class="material-icons text-blue-500 float-left mr-2">forum</i>
<div>by <%= link_to topic.user.display_name, topic.user %></div>
</div>
</div>
<% if thredded_current_user %>
<% if topic.followed? %>
<div class="thredded--topic-header--follow-info w-full">
<div class="thredded--topic-header--follow-info--reason block">
<%=
button_to topic.unfollow_path,
form: {
class: 'thredded--topic-header--follow-info--unfollow ' +
'w-full bg-blue-400 hover:bg-blue-500 cursor-pointer rounded py-2 mb-2 text-center'
} do
%>
<span class="text-white">
<%= t('thredded.topics.unfollow') %>
<%= inline_svg_tag 'thredded/follow.svg', class: 'thredded--topic-header--follow-icon' %>
</span>
<% end %>
<p class="text-xs text-left">
<%= topic_follow_reason_text topic.follow_reason %>
</p>
</div>
</div>
<% else %>
<div class="thredded--topic-header--follow-info w-full">
<%=
button_to topic.follow_path,
form: {
class: 'thredded--topic-header--follow-info--follow ' +
'w-full bg-blue-400 hover:bg-blue-500 cursor-pointer rounded py-2 mb-2 text-center'
} do
%>
<span class="text-white">
<%= t('thredded.topics.follow') %>
<%= inline_svg_tag 'thredded/follow.svg', class: 'thredded--topic-header--follow-icon' %>
</span>
<% end %>
</div>
<!-- Topic Actions (Follow button and dropdown) -->
<div class="flex items-center gap-2">
<!-- Follow/Unfollow Button -->
<% if thredded_current_user %>
<% if topic.followed? %>
<%= button_to topic.unfollow_path,
method: :post,
class: 'inline-flex items-center px-3 py-1.5 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-colors' do %>
<i class="material-icons text-base mr-1">notifications_off</i>
<%= t('thredded.topics.unfollow') %>
<% end %>
<% else %>
<%= button_to topic.follow_path,
method: :post,
class: 'inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-lg text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-colors' do %>
<i class="material-icons text-base mr-1">notifications</i>
<%= t('thredded.topics.follow') %>
<% end %>
<% end %>
<% end %>
<div class="clear-both">
<%= render partial: 'thredded/topics/followers', locals: {topic: topic} %>
</div>
<!-- Actions Dropdown -->
<div class="relative inline-block text-left">
<button type="button"
class="inline-flex items-center px-3 py-1.5 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-colors"
onclick="this.nextElementSibling.classList.toggle('hidden')">
<i class="material-icons">more_vert</i>
</button>
<div class="mt-32">thread actions todo</div>
<div class="thredded--post--dropdown">
<i class="material-icons thredded--post--dropdown--toggle">tune</i>
<div class="thredded--post--dropdown--actions">
<div class="hidden origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50">
<div class="py-1" role="menu">
<% if topic.can_update? %>
<%= link_to t('thredded.topics.edit'), topic.edit_path,
class: 'thredded--post--dropdown--actions--item',
class: 'block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100',
rel: 'nofollow' %>
<% end %>
<% if user_signed_in? && params.key?(:id) %>
<%= link_to main_app.documentize_topic_path(params[:id]), class: 'thredded--post--dropdown--actions--item' do %>
<%= link_to main_app.documentize_topic_path(params[:id]),
class: 'block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100' do %>
<i class="material-icons text-base mr-2 align-middle">description</i>
Export to document
<% end %>
<%= link_to main_app.plaintext_topic_path(params[:id]), class: 'thredded--post--dropdown--actions--item', rel: 'nofollow' do %>
<%= link_to main_app.plaintext_topic_path(params[:id]),
class: 'block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100',
rel: 'nofollow' do %>
<i class="material-icons text-base mr-2 align-middle">text_fields</i>
View as plaintext
<% end %>
<%= link_to main_app.irc_log_topic_path(params[:id]), class: 'thredded--post--dropdown--actions--item', rel: 'nofollow' do %>
<%= link_to main_app.irc_log_topic_path(params[:id]),
class: 'block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100',
rel: 'nofollow' do %>
<i class="material-icons text-base mr-2 align-middle">chat</i>
View as chat log
<% end %>
<% end %>
<% if topic.can_destroy? %>
<div class="thredded--topic-delete--wrapper">
<%= button_to t('thredded.topics.delete_topic'), topic.destroy_path, method: :delete,
form_class: 'thredded--topic-delete-form',
class: 'thredded--button thredded--post--dropdown--actions--item',
'data-confirm' => t('thredded.topics.delete_confirm') %>
</div>
<hr class="my-1">
<%= link_to topic.destroy_path,
method: :delete,
data: { confirm: t('thredded.topics.delete_confirm') },
class: 'block px-4 py-2 text-sm text-red-700 hover:bg-red-50' do %>
<i class="material-icons text-base mr-2 align-middle">delete</i>
<%= t('thredded.topics.delete_topic') %>
<% end %>
<% end %>
</div>
</div>
<% end %>
</header>
</div>
</div>

View File

@ -1,56 +1,112 @@
<%= content_tag :article,
id: dom_id(topic),
class: ['thredded--topics--topic', topic_css_classes(topic)],
id: dom_id(topic),
class: ['group hover:bg-gray-50 transition-colors duration-150',
topic.sticky? ? 'bg-yellow-50 hover:bg-yellow-100' : '',
topic.locked? ? 'opacity-75' : ''],
data: {topic: topic.id, messageboard: topic.messageboard_id} do %>
<div class="thredded--topics--posts-count"><%= topic.posts_count %></div>
<div class="thredded--topics--follow-info" title="<%= topic_follow_reason_text topic.follow_reason %>">
<svg class="thredded--topics--follow-icon" viewBox="0 0 116 121" role="img">
<% if topic.followed? %>
<use xlink:href="#thredded-follow-icon"></use>
<% else %>
<use xlink:href="#thredded-unfollow-icon"></use>
<% end %>
</svg>
</div>
<h1 class="thredded--topics--title">
<% if user_signed_in? %>
<%
@current_user_unread_and_followed_topics ||= Thredded::Topic.unread_followed_by(current_user).pluck(:id)
if @current_user_unread_and_followed_topics.include?(topic.id)
%>
<i class="material-icons amber-text tooltipped" style="font-size: 0.9em"
data-tooltip="You are following this thread and there are unread replies."
data-position="right">
notifications_active
</i>
<% end %>
<div class="px-6 py-4">
<div class="flex items-center">
<!-- Topic Title and Meta -->
<div class="flex-1 min-w-0">
<div class="flex items-start">
<div class="flex-1">
<h3 class="text-base font-medium text-gray-900 group-hover:text-purple-600 transition-colors flex items-center">
<!-- Status Icons -->
<% if topic.sticky? %>
<i class="material-icons text-yellow-600 mr-2 text-base" title="Pinned topic">push_pin</i>
<% end %>
<% if topic.locked? %>
<i class="material-icons text-gray-500 mr-2 text-base" title="Locked topic">lock</i>
<% end %>
<!-- Follow/Unread Notification -->
<% if user_signed_in? && topic.followed? %>
<%
@current_user_unread_and_followed_topics ||= Thredded::Topic.unread_followed_by(current_user).pluck(:id)
has_unread = @current_user_unread_and_followed_topics.include?(topic.id)
%>
<% if has_unread %>
<i class="material-icons text-amber-500 mr-2 text-base animate-pulse"
title="Following • New replies">
notifications_active
</i>
<% else %>
<i class="material-icons text-gray-400 mr-2 text-base"
title="Following • All caught up">
notifications
</i>
<% end %>
<% end %>
<!-- Topic Title -->
<%= link_to topic.title, topic.path, class: 'hover:underline' %>
<!-- Unread Badge -->
<% if !topic.read? %>
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
NEW
</span>
<% end %>
</h3>
<!-- Categories -->
<% if topic.categories.any? %>
<div class="mt-1 flex flex-wrap gap-1">
<% topic.categories.each do |category| %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700">
<%= category.name %>
</span>
<% end %>
</div>
<% end %>
<!-- Topic Meta Info -->
<div class="mt-1 flex items-center text-sm text-gray-500">
<span>Started by <%= user_link topic.user %></span>
<span class="mx-2">•</span>
<span><%= time_ago topic.created_at %></span>
</div>
</div>
</div>
</div>
<!-- Posts Count -->
<div class="hidden sm:flex w-32 justify-center">
<div class="text-center">
<div class="text-2xl font-semibold text-gray-900"><%= topic.posts_count - 1 %></div>
<div class="text-xs text-gray-500"><%= 'reply'.pluralize(topic.posts_count - 1) %></div>
</div>
</div>
<!-- Last Activity -->
<div class="hidden md:flex w-48 justify-end">
<div class="text-right text-sm">
<div class="text-gray-900 font-medium"><%= user_link topic.last_user %></div>
<div class="text-gray-500"><%= time_ago topic.last_post_at %></div>
</div>
</div>
</div>
<!-- Moderation State -->
<% if topic.blocked? && topic.can_moderate? %>
<div class="mt-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-red-100 text-red-800">
<i class="material-icons text-base mr-1">block</i>
<%= render 'thredded/shared/content_moderation_blocked_state', moderation_record: topic.last_moderation_record %>
</span>
</div>
<% end %>
<%= link_to topic.title, topic.path %>
</h1>
<% if topic.categories.any? %>
<ul class="thredded--topics--categories">
<%= render topic.categories %>
</ul>
<% end %>
<cite class="thredded--topics--updated-by">
last post
<%= time_ago topic.last_post_at %>
by
<%= user_link topic.last_user %>
</cite>
<% if topic.blocked? && topic.can_moderate? %>
<span class="thredded--topics--moderation-state thredded--alert thredded--alert-danger">
<%= render 'thredded/shared/content_moderation_blocked_state', moderation_record: topic.last_moderation_record %>
</span>
<% end %>
</div>
<% end %>
<% if local_assigns[:sticky_topics_divider] &&
!topic_iteration.last? && topic.sticky? && !topics[topic_counter + 1].sticky? %>
<%= render 'thredded/topics/sticky_topics_divider' %>
<% end %>
<div class="px-6 py-2 bg-gray-100 border-y border-gray-200">
<div class="flex items-center justify-center text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex-1 h-px bg-gray-300"></div>
<span class="px-3">Regular Topics</span>
<div class="flex-1 h-px bg-gray-300"></div>
</div>
</div>
<% end %>

View File

@ -0,0 +1,101 @@
<% content_for :thredded_page_title, messageboard.name %>
<% content_for :thredded_page_id, 'thredded--topics-index' %>
<% content_for :thredded_breadcrumbs, render('thredded/shared/breadcrumbs') %>
<%= thredded_page do %>
<%= define_svg_icons('thredded/follow.svg', 'thredded/unfollow.svg') %>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Page Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900 flex items-center">
<i class="material-icons text-blue-600 mr-3 text-3xl">forum</i>
<%= messageboard.name %>
</h1>
<% if messageboard.description.present? %>
<p class="text-gray-600 mt-2"><%= messageboard.description %></p>
<% end %>
</div>
<% if messageboard.topics_count > 0 %>
<div class="flex items-center gap-4 text-sm text-gray-500">
<div class="flex items-center">
<i class="material-icons text-base mr-1">chat_bubble_outline</i>
<span><%= number_with_delimiter(messageboard.topics_count) %> <%= 'topic'.pluralize(messageboard.topics_count) %></span>
</div>
<div class="flex items-center">
<i class="material-icons text-base mr-1">forum</i>
<span><%= number_with_delimiter(messageboard.posts_count) %> <%= 'post'.pluralize(messageboard.posts_count) %></span>
</div>
</div>
<% end %>
</div>
</div>
<!-- New Topic Form -->
<% if @new_topic %>
<div class="mb-6">
<%= render 'thredded/topics/form',
topic: @new_topic,
css_class: 'bg-white rounded-lg shadow-sm border border-gray-200 p-6',
placeholder: t('thredded.topics.form.title_placeholder_start') %>
</div>
<% end %>
<!-- Topics List -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden"
data-thredded-topics="true"
data-thredded-topic-posts-per-page="<%= Thredded.posts_per_page %>">
<% if @topics.to_a.any? %>
<!-- Topics Header -->
<div class="bg-gray-50 px-6 py-3 border-b border-gray-200">
<div class="flex items-center text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex-1">Topic</div>
<div class="hidden sm:block w-32 text-center">Replies</div>
<div class="hidden md:block w-48 text-right">Last Activity</div>
</div>
</div>
<!-- Topics -->
<div class="divide-y divide-gray-200">
<%= render partial: 'thredded/topics/topic',
collection: @topics,
locals: {
sticky_topics_divider: true,
topics: @topics.to_a
} %>
</div>
<% else %>
<!-- Empty State -->
<div class="text-center py-12">
<i class="material-icons text-6xl text-gray-300 mb-4">forum</i>
<h3 class="text-lg font-medium text-gray-900 mb-2">No topics yet</h3>
<p class="text-gray-500 mb-6">Be the first to start a discussion!</p>
<% if @new_topic %>
<button type="button"
onclick="document.querySelector('#new_topic input[name=\"topic[title]\"]').focus();"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
<i class="material-icons mr-2 text-lg">add</i>
Start a New Topic
</button>
<% end %>
</div>
<% end %>
</div>
<!-- Pagination -->
<% if @topics.respond_to?(:total_pages) && @topics.total_pages > 1 %>
<div class="mt-6">
<%= paginate @topics, theme: 'tailwind' %>
</div>
<% end %>
<!-- Messageboard Actions -->
<div class="mt-8">
<%= render 'thredded/messageboards/messageboard_actions', messageboard: messageboard %>
</div>
</div>
<% end %>

View File

@ -0,0 +1,186 @@
<% content_for :thredded_page_title, t('thredded.topics.search.page_title') %>
<% content_for :thredded_page_id, 'thredded--topic-search-results' %>
<% content_for :thredded_breadcrumbs, render('thredded/shared/breadcrumbs') %>
<%= thredded_page do %>
<!-- Search Header -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 mb-6">
<div class="px-6 py-5 border-b border-gray-200 bg-gradient-to-r from-blue-50 to-white">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="flex items-center justify-center w-10 h-10 bg-blue-100 rounded-full">
<i class="material-icons text-blue-600 text-xl">search</i>
</div>
<div>
<h1 class="text-xl font-semibold text-gray-900">Search Results</h1>
<p class="text-sm text-gray-600 mt-0.5">
Searching for "<span class="font-medium text-gray-900"><%= @query %></span>"
</p>
</div>
</div>
<!-- New Search Form -->
<%= form_tag messageboards_search_path, method: :get, class: 'flex items-center space-x-2' do %>
<input type="search"
name="q"
value="<%= @query %>"
placeholder="Search forums..."
class="px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-48">
<button type="submit" class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm hover:bg-blue-700 transition-colors duration-150 flex items-center">
<i class="material-icons text-base">search</i>
</button>
<% end %>
</div>
</div>
<% if @topics.present? %>
<!-- Results Count -->
<div class="px-6 py-3 bg-green-50 border-b border-green-100">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<i class="material-icons text-green-600 text-base">check_circle</i>
<span class="text-sm text-green-800">
<% if messageboard_or_nil %>
Found results in <span class="font-medium"><%= messageboard.name %></span>
<% else %>
Found <span class="font-medium"><%= @topics.total_count %></span> matching <%= 'topic'.pluralize(@topics.total_count) %>
<% end %>
</span>
</div>
<% if messageboard_or_nil %>
<a href="<%= messageboards_search_path(q: @query) %>"
class="text-sm text-blue-600 hover:text-blue-800 font-medium flex items-center space-x-1">
<span><%= t('thredded.topics.search.search_in_all_messageboards_btn') %></span>
<i class="material-icons text-base">arrow_forward</i>
</a>
<% end %>
</div>
</div>
<% end %>
</div>
<% if @topics.present? %>
<!-- Search Results -->
<div class="space-y-3">
<% @topics.each do |topic| %>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow duration-200">
<%= link_to topic_path(topic), class: 'block p-4 hover:bg-gray-50 transition-colors duration-150' do %>
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2 mb-2">
<% if topic.sticky? %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="material-icons text-xs mr-0.5">push_pin</i>
Pinned
</span>
<% end %>
<% if topic.locked? %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700">
<i class="material-icons text-xs mr-0.5">lock</i>
Locked
</span>
<% end %>
<% if topic.categories.any? %>
<% topic.categories.each do |category| %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
style="background-color: <%= category.color %>20; color: <%= category.color %>;">
<%= category.name %>
</span>
<% end %>
<% end %>
</div>
<h3 class="text-base font-medium text-gray-900 mb-1">
<%= topic.title %>
</h3>
<div class="flex items-center space-x-4 text-sm text-gray-600">
<div class="flex items-center space-x-1">
<% if topic.user %>
<img src="<%= topic.user.thredded_avatar_url %>"
alt="<%= topic.user.thredded_display_name %>"
class="w-5 h-5 rounded-full">
<span><%= topic.user.thredded_display_name %></span>
<% else %>
<i class="material-icons text-base">person</i>
<span>Anonymous</span>
<% end %>
</div>
<div class="flex items-center space-x-1">
<i class="material-icons text-base">schedule</i>
<span><%= time_ago_in_words(topic.created_at) %> ago</span>
</div>
<div class="flex items-center space-x-1">
<i class="material-icons text-base">forum</i>
<span><%= pluralize(topic.posts_count, 'reply', 'replies') %></span>
</div>
</div>
</div>
<div class="ml-4 flex-shrink-0 text-right">
<div class="text-sm text-gray-500">
<% if topic.last_user %>
<div class="flex items-center justify-end space-x-1 mb-1">
<span>Last by</span>
<img src="<%= topic.last_user.thredded_avatar_url %>"
alt="<%= topic.last_user.thredded_display_name %>"
class="w-4 h-4 rounded-full">
</div>
<% end %>
<div class="text-xs text-gray-400">
<%= time_ago_in_words(topic.last_post_at) %> ago
</div>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
<!-- Pagination -->
<div class="mt-6">
<%= paginate @topics %>
</div>
<% else %>
<!-- No Results -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
<div class="flex justify-center mb-4">
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center">
<i class="material-icons text-gray-400 text-3xl">search_off</i>
</div>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">No results found</h3>
<p class="text-gray-600 mb-6">
<% if messageboard_or_nil %>
No topics found matching "<%= @query %>" in <span class="font-medium"><%= messageboard.name %></span>
<% else %>
No topics found matching "<%= @query %>"
<% end %>
</p>
<div class="space-y-3">
<% if messageboard_or_nil %>
<a href="<%= messageboards_search_path(q: @query) %>"
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md text-sm hover:bg-blue-700 transition-colors duration-150">
<i class="material-icons text-base mr-1">search</i>
<%= t('thredded.topics.search.search_in_all_messageboards_btn') %>
</a>
<% end %>
<div class="text-sm text-gray-500">
<p>Try adjusting your search terms or browse the forums:</p>
<a href="<%= thredded.root_path %>" class="text-blue-600 hover:text-blue-800 font-medium">
View all forums →
</a>
</div>
</div>
</div>
<% end %>
<% end %>

View File

@ -1,52 +1,151 @@
<% topic = @posts.topic %>
<% content_for :thredded_page_title, topic.title %>
<% content_for :thredded_page_id, 'thredded--topic-show' %>
<% content_for :thredded_breadcrumbs, render('thredded/shared/breadcrumbs') %>
<div class="flex-1 relative z-0 flex">
<aside class="bg-gray-50 hidden relative xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200 overflow-y-auto">
<!-- side panel (hidden on small screens; todo: probably just stack it) -->
<div class="py-6 px-4 sm:px-6 lg:px-8">
<%= content_tag :section,
class: ['thredded--main-section', 'thredded--topic', *topic_css_classes(topic)] do %>
<%= render 'thredded/topics/header', topic: topic %>
<% end %>
</div>
</aside>
<main class="flex-1 relative z-0 mt-10 focus:outline-none">
<!-- main panel-->
<%= thredded_page do %>
<%= content_tag :section,
id: dom_id(topic),
class: ['thredded--main-section', 'thredded--topic', *topic_css_classes(topic)] do %>
<%= render_posts @posts,
partial: 'thredded/posts/post',
content_partial: 'thredded/posts/content',
locals: { actions: { quote: true } } %>
<%= view_hooks.posts_common.pagination_bottom.render(self, posts: @posts) do %>
<footer class="thredded--pagination-bottom"><%= paginate @posts %></footer>
<%= thredded_page do %>
<!-- Full-width Topic Header -->
<div class="topic-header-section">
<div class="topic-header-container">
<!-- Topic Title -->
<h1 class="topic-title">
<% if topic.sticky? %>
<i class="material-icons" title="Pinned topic">push_pin</i>
<% end %>
<% if topic.locked? %>
<p class="thredded--topic--locked-notice"><%= t 'thredded.topics.locked.message'%></p>
<i class="material-icons" title="Locked topic">lock</i>
<% end %>
<% if policy(@new_post.post).create? %>
<div class="thredded--post-form--wrapper">
<h3 class="thredded--post-form--title"><%= t('thredded.posts.form.title_label') %></h3>
<%= render 'thredded/posts/form',
post: @new_post,
button_text: t('thredded.posts.form.create_btn'),
button_submitting_text: t('thredded.posts.form.create_btn_submitting') %>
<%= topic.title %>
</h1>
<!-- Topic Metadata -->
<div class="topic-meta">
<div class="meta-item">
<i class="material-icons">person_outline</i>
<span>Started by <%= user_link topic.user %></span>
</div>
<div class="meta-item">
<i class="material-icons">schedule</i>
<span><%= time_ago topic.created_at %></span>
</div>
<div class="meta-item">
<i class="material-icons">forum</i>
<span><%= topic.posts_count %> <%= 'post'.pluralize(topic.posts_count) %></span>
</div>
<% if topic.last_user %>
<div class="meta-item">
<i class="material-icons">update</i>
<span>Last reply by <%= user_link topic.last_user %> <%= time_ago topic.last_post_at %></span>
</div>
<% end %>
</div>
<!-- Categories -->
<% if topic.categories.any? %>
<div class="topic-categories">
<% topic.categories.each do |category| %>
<span class="category-badge"><%= category.name %></span>
<% end %>
</div>
<% end %>
<!-- Topic Actions -->
<div class="topic-actions">
<% if thredded_current_user %>
<% if topic.followed? %>
<%= button_to topic.unfollow_path,
method: :post,
class: 'following',
title: topic_follow_reason_text(topic.follow_reason) do %>
<i class="material-icons">notifications_active</i>
Following
<% end %>
<% else %>
<%= button_to topic.follow_path,
method: :post,
title: "Follow this topic to get notifications" do %>
<i class="material-icons">notifications_none</i>
Follow
<% end %>
<% end %>
<% end %>
<% if topic.can_update? %>
<%= link_to topic.edit_path, rel: 'nofollow' do %>
<i class="material-icons">edit</i>
Edit Topic
<% end %>
<% end %>
<% if topic.can_destroy? %>
<div class="thredded--topic-delete--wrapper">
<%= button_to t('thredded.topics.delete_topic'), topic.destroy_path, method: :delete,
form_class: 'thredded--topic-delete-form',
class: 'thredded--button bg-red-500',
'data-confirm' => t('thredded.topics.delete_confirm') %>
</div>
<%= button_to topic.destroy_path,
method: :delete,
data: { confirm: t('thredded.topics.delete_confirm') },
class: 'text-red-600 hover:bg-red-50' do %>
<i class="material-icons">delete</i>
Delete Topic
<% end %>
<% end %>
</div>
</div>
</div>
<!-- Narrower Content Area -->
<div class="topic-content-area">
<%= content_tag :section,
id: dom_id(topic),
class: ['thredded--topic', *topic_css_classes(topic)] do %>
<!-- Top Pagination -->
<% if @posts.respond_to?(:total_pages) && @posts.total_pages > 1 %>
<div class="topic-pagination">
<%= paginate @posts %>
</div>
<% end %>
<!-- Posts Container -->
<div class="posts-container">
<%= render_posts @posts,
partial: 'thredded/posts/post',
content_partial: 'thredded/posts/content',
locals: { actions: { quote: true } } %>
</div>
<!-- Bottom Pagination -->
<% if @posts.respond_to?(:total_pages) && @posts.total_pages > 1 %>
<div class="topic-pagination">
<%= paginate @posts %>
</div>
<% end %>
<!-- Locked Topic Notice -->
<% if topic.locked? %>
<div class="locked-notice">
<i class="material-icons">lock</i>
<p><%= t 'thredded.topics.locked.message' %></p>
</div>
<% end %>
<!-- Reply Form -->
<% if policy(@new_post.post).create? %>
<div class="reply-form-section">
<div class="reply-form-header">
<h3>
<i class="material-icons">reply</i>
<%= t('thredded.posts.form.title_label') %>
</h3>
</div>
<div class="reply-form-body">
<%= render 'thredded/posts/form',
post: @new_post,
button_text: t('thredded.posts.form.create_btn'),
button_submitting_text: t('thredded.posts.form.create_btn_submitting') %>
</div>
</div>
<% end %>
<% end %>
</main>
</div>
</div>
<% end %>

View File

@ -12,6 +12,7 @@ Rails.application.config.assets.paths << Rails.root.join('node_modules')
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )
Rails.application.config.assets.precompile += %w( thredded.js )
# Include Rails helpers in the assets pipeline
# This might be helpful for including e.g. link_to in js components, but is commented out until needed