Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions app/controllers/topics_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class TopicsController < ApplicationController
before_action :verify_user, except: [ :index, :show ]

def index
topics = Topic.all
.includes(:user, :category, messages: :user)
Expand All @@ -23,4 +25,30 @@ def show
])
render inertia: { topic: }
end

def new
topic = Topic.new(category_id: params.dig(:topic, :category_id))

render inertia: { topic: }
end

def create
topic = current_user.topics.build(topic_params)

if topic.save(context: :form_submission)
redirect_to topic, notice: "Topic created successfully"
else
redirect_to new_topic_path, inertia: { errors: topic.errors }
end
end

private

def topic_params
params.require(:topic).permit(:title, :category_id, messages_attributes: [ :body ])
end

def verify_user
redirect_to topics_path unless current_user
end
end
21 changes: 17 additions & 4 deletions app/frontend/pages/categories/show.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { Link } from "@inertiajs/react";
import TopicsTable from "../../components/TopicsTable"
import AppLayout from "../../layouts/AppLayout"
import { Category } from "../../types"
import { Category, User } from "../../types"

function CategoriesShow({ category }: { category: Category }) {
function CategoriesShow({ category, current_user: currentUser }: { category: Category, current_user: User }) {
const { topics } = category;

return (
<AppLayout>
<h1 className="text-2xl font-bold text-gray-900 mb-6">Category: {category.name}</h1>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-gray-900">Category: {category.name}</h1>
{currentUser && (
<Link
href={'/topics/new'}
data={{topic: { category_id: category.id }}}
className="bg-sky-600 hover:bg-sky-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors shadow inline-block text-center"
>
New Topic
</Link>
)}
</div>

<div className="max-w-7xl mx-auto">
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
<TopicsTable topics={topics} />
Expand All @@ -17,4 +30,4 @@ function CategoriesShow({ category }: { category: Category }) {
)
}

export default CategoriesShow
export default CategoriesShow
18 changes: 15 additions & 3 deletions app/frontend/pages/my_topics/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { Head } from '@inertiajs/react'
import { Head, Link } from '@inertiajs/react'
import AppLayout from '../../layouts/AppLayout'
import { Topic } from '../../types'
import { Topic, User } from '../../types'
import TopicsTable from '../../components/TopicsTable'

function MyTopicsIndex({ topics }: { topics: Topic[] }) {
function MyTopicsIndex({ topics, current_user: currentUser }: { topics: Topic[], current_user: User }) {
return (
<AppLayout>
<Head title="My Topics - Pups & Pourovers" />

<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-gray-900">My Topics</h1>
{currentUser && (
<Link
href="/topics/new"
className="bg-sky-600 hover:bg-sky-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors shadow inline-block text-center"
>
New Topic
</Link>
)}
</div>

<div className="max-w-7xl mx-auto">
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
<TopicsTable topics={topics} />
Expand Down
16 changes: 13 additions & 3 deletions app/frontend/pages/topics/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { Head } from '@inertiajs/react'
import { Head, Link } from '@inertiajs/react'
import AppLayout from '../../layouts/AppLayout'
import { Topic } from '../../types'
import { Topic, User } from '../../types'
import TopicsTable from '../../components/TopicsTable'

function TopicsIndex({ topics }: { topics: Topic[] }) {
function TopicsIndex({ topics, current_user: currentUser }: { topics: Topic[], current_user: User }) {
return (
<AppLayout>
<Head title="Topics - Pups & Pourovers" />

<div className="max-w-7xl mx-auto">
<div className="flex justify-end mb-6">
{currentUser && (
<Link
href="/topics/new"
className="bg-sky-600 hover:bg-sky-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors shadow inline-block text-center"
>
New Topic
</Link>
)}
</div>
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
<TopicsTable topics={topics} />
</div>
Expand Down
114 changes: 114 additions & 0 deletions app/frontend/pages/topics/new.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {Head, Form, usePage} from '@inertiajs/react'
import AppLayout from '../../layouts/AppLayout'
import LexicalRichTextEditor from '../../components/LexicalRichTextEditor'

interface TopicFormProps {
topic?: {
title?: string
category_id?: number
messages_attributes?: Array<{ body: string }>
}
}

const TopicsNew = ({topic}: TopicFormProps) => {
const {categories} = usePage().props

return (
<AppLayout>
<Head title="New Topic - Pups & Pourovers"/>

<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">New Topic</h1>
<Form
action="/topics"
method="post"
disableWhileProcessing
className="inert:opacity-50 inert:pointer-events-none"
>
{({errors, processing}) => (
<>
<div className="mb-6">
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
Topic Title
</label>
<input
type="text"
id="title"
name="topic[title]"
defaultValue={topic?.title}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-sky-500 focus:border-sky-500"
placeholder="Enter topic title..."
/>
{(errors.title) && (
<p className="mt-1 text-sm text-red-600">
Title {errors.title}
</p>
)}
</div>

<div className="mb-6">
<label htmlFor="category_id" className="block text-sm font-medium text-gray-700 mb-2">
Category
</label>
<select
id="category_id"
name="topic[category_id]"
defaultValue={topic?.category_id}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-sky-500 focus:border-sky-500"
>
<option value="">Select a category...</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
{(errors.category) && (
<p className="mt-1 text-sm text-red-600">
Category {errors.category}
</p>
)}
</div>

<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Message
</label>
<div className="mb-2">
<LexicalRichTextEditor
name="topic[messages_attributes][0][body]"
placeholder="Write your message..."
/>
</div>
{(errors['messages[0].body']) && (
<p className="mt-1 text-sm text-red-600">
{errors['messages[0].body']}
</p>
)}
{(errors.messages) && (
<p className="mt-1 text-sm text-red-600">
Messages {errors.messages}
</p>
)}
</div>

<div className="flex items-center justify-end gap-4">
<button
type="submit"
disabled={processing}
className="px-6 py-3 bg-sky-600 text-white font-medium rounded-lg hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
>
{processing ? 'Creating Topic...' : 'Create Topic'}
</button>
</div>
</>
)}
</Form>
</div>
</div>
</AppLayout>
)
}

export default TopicsNew
15 changes: 14 additions & 1 deletion app/models/topic.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
class Topic < ApplicationRecord
has_many :messages, -> { order(created_at: :asc) }, dependent: :destroy
has_many :messages, -> { order(created_at: :asc) }, dependent: :destroy, index_errors: true
belongs_to :user, counter_cache: true
belongs_to :category, counter_cache: true

accepts_nested_attributes_for :messages, reject_if: :all_blank

validates :title, presence: true
validates :category_id, presence: true

validates :messages, presence: true, on: :form_submission

before_validation :assign_user_to_messages, on: [ :create, :form_submission ]

private

def assign_user_to_messages
messages.each { |message| message.user_id ||= user_id }
end
end
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# root "posts#index"
root "landing#index"

resources :topics, only: [ :index, :show ] do
resources :topics, only: [ :index, :show, :new, :create ] do
resources :messages, only: [ :create ]
end
resources :categories, only: [ :show ]
Expand Down