mirror of
https://github.com/indentlabs/notebook.git
synced 2025-10-26 11:19:22 +00:00
forums styling
This commit is contained in:
parent
a7178ab3a6
commit
ec9fa08b81
@ -13,3 +13,8 @@
|
||||
*= require font-awesome
|
||||
*= require_tree .
|
||||
*/
|
||||
|
||||
/* Alpine.js x-cloak - hide elements until Alpine loads */
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
217
app/assets/stylesheets/currently-online.scss
Normal file
217
app/assets/stylesheets/currently-online.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
268
app/assets/stylesheets/forum-messageboards.scss
Normal file
268
app/assets/stylesheets/forum-messageboards.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/assets/stylesheets/forum-navigation.scss
Normal file
57
app/assets/stylesheets/forum-navigation.scss
Normal 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;
|
||||
}
|
||||
415
app/assets/stylesheets/forum-topics.scss
Normal file
415
app/assets/stylesheets/forum-topics.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1483
app/assets/stylesheets/moderation.scss
Normal file
1483
app/assets/stylesheets/moderation.scss
Normal file
File diff suppressed because it is too large
Load Diff
546
app/assets/stylesheets/private-message-compose.scss
Normal file
546
app/assets/stylesheets/private-message-compose.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
524
app/assets/stylesheets/private-messages.scss
Normal file
524
app/assets/stylesheets/private-messages.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
31
app/assets/stylesheets/thredded-overrides.scss.bak
Normal file
31
app/assets/stylesheets/thredded-overrides.scss.bak
Normal 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;
|
||||
}
|
||||
540
app/assets/stylesheets/topic-show.scss
Normal file
540
app/assets/stylesheets/topic-show.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %>
|
||||
|
||||
@ -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 %>
|
||||
|
||||
@ -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 %>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %>
|
||||
65
app/views/thredded/messageboards/index.html.erb
Normal file
65
app/views/thredded/messageboards/index.html.erb
Normal 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 %>
|
||||
38
app/views/thredded/moderation/_nav.html.erb
Normal file
38
app/views/thredded/moderation/_nav.html.erb
Normal 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 %>
|
||||
@ -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>
|
||||
125
app/views/thredded/moderation/activity.html.erb
Normal file
125
app/views/thredded/moderation/activity.html.erb
Normal 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 %>
|
||||
154
app/views/thredded/moderation/history.html.erb
Normal file
154
app/views/thredded/moderation/history.html.erb
Normal 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 %>
|
||||
90
app/views/thredded/moderation/pending.html.erb
Normal file
90
app/views/thredded/moderation/pending.html.erb
Normal 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 %>
|
||||
139
app/views/thredded/moderation/users.html.erb
Normal file
139
app/views/thredded/moderation/users.html.erb
Normal 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 %>
|
||||
@ -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 %>
|
||||
55
app/views/thredded/posts_common/_actions.html.erb
Normal file
55
app/views/thredded/posts_common/_actions.html.erb
Normal 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 %>
|
||||
22
app/views/thredded/posts_common/_content.html.erb
Normal file
22
app/views/thredded/posts_common/_content.html.erb
Normal 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>
|
||||
33
app/views/thredded/posts_common/_form.html.erb
Normal file
33
app/views/thredded/posts_common/_form.html.erb
Normal 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 %>
|
||||
8
app/views/thredded/posts_common/actions/_delete.html.erb
Normal file
8
app/views/thredded/posts_common/actions/_delete.html.erb
Normal 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 %>
|
||||
6
app/views/thredded/posts_common/actions/_edit.html.erb
Normal file
6
app/views/thredded/posts_common/actions/_edit.html.erb
Normal 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 %>
|
||||
@ -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 %>
|
||||
7
app/views/thredded/posts_common/actions/_quote.html.erb
Normal file
7
app/views/thredded/posts_common/actions/_quote.html.erb
Normal 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 %>
|
||||
7
app/views/thredded/posts_common/actions/_report.html.erb
Normal file
7
app/views/thredded/posts_common/actions/_report.html.erb
Normal 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 %>
|
||||
@ -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>
|
||||
|
||||
23
app/views/thredded/posts_common/form/_preview_area.html.erb
Normal file
23
app/views/thredded/posts_common/form/_preview_area.html.erb
Normal 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 %>
|
||||
78
app/views/thredded/private_topics/_form.html.erb
Normal file
78
app/views/thredded/private_topics/_form.html.erb
Normal 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>
|
||||
@ -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>
|
||||
49
app/views/thredded/private_topics/_private_topic.html.erb
Normal file
49
app/views/thredded/private_topics/_private_topic.html.erb
Normal 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 %>
|
||||
75
app/views/thredded/private_topics/index.html.erb
Normal file
75
app/views/thredded/private_topics/index.html.erb
Normal 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 %>
|
||||
25
app/views/thredded/private_topics/new.html.erb
Normal file
25
app/views/thredded/private_topics/new.html.erb
Normal 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 %>
|
||||
@ -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 %>
|
||||
|
||||
35
app/views/thredded/shared/_currently_online.html.erb
Normal file
35
app/views/thredded/shared/_currently_online.html.erb
Normal 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 %>
|
||||
@ -0,0 +1,5 @@
|
||||
<header>
|
||||
<h3 class="thredded--currently-online--title collapsed" data-count="<%= users.count %>">
|
||||
<%= t "thredded.users.currently_online" %>
|
||||
</h3>
|
||||
</header>
|
||||
@ -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 %>
|
||||
@ -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 %>
|
||||
@ -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>
|
||||
160
app/views/thredded/topics/_form.html.erb
Normal file
160
app/views/thredded/topics/_form.html.erb
Normal 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>
|
||||
@ -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>
|
||||
@ -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 %>
|
||||
101
app/views/thredded/topics/index.html.erb
Normal file
101
app/views/thredded/topics/index.html.erb
Normal 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 %>
|
||||
186
app/views/thredded/topics/search.html.erb
Normal file
186
app/views/thredded/topics/search.html.erb
Normal 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 %>
|
||||
@ -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 %>
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user