Skip to content

Commit 5316334

Browse files
committed
✨ Update Dependencies and Enhance Article Management
- Update package.json and package-lock.json to include new dependencies: @tanstack/react-query, date-fns, lucide-react, react-syntax-highlighter, socket.io, and wavesurfer.js. - Refactor ArticlesController to improve article creation logic and utilize enums for article status. - Enhance HomeController and ProfileController to filter articles based on their status. - Update Vite configuration to include a proxy for socket.io connections. - Modify article model to improve tag handling and slug generation. - Add new routes for message and discussion management, enhancing application functionality.
1 parent 0606e34 commit 5316334

30 files changed

Lines changed: 2951 additions & 47 deletions

app/controllers/articles_controller.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { HttpContext } from '@adonisjs/core/http'
2-
import { articleValidator } from '#validators/article_validator'
1+
import { ArticleStatus } from '#enums/article_status'
32
import Article from '#models/article'
4-
import { DateTime } from 'luxon'
53
import ArticleStatsService from '#services/article_stats_service'
6-
import { ArticleStatus } from '#enums/article_status'
4+
import { articleValidator } from '#validators/article_validator'
5+
import { HttpContext } from '@adonisjs/core/http'
6+
import { DateTime } from 'luxon'
77

88
export default class ArticlesController {
99
async index({ inertia, request }: HttpContext) {
@@ -25,22 +25,20 @@ export default class ArticlesController {
2525

2626
async store({ request, auth, response }: HttpContext) {
2727
const data = await articleValidator.validate(request.all())
28-
const article = new Article()
2928

30-
article.title = data.title
31-
article.content = data.content
32-
article.excerpt = data.excerpt
33-
article.status = data.status as ArticleStatus
34-
article.authorId = auth.user!.id
29+
const payload: Partial<Article> = { ...data, authorId: auth.user!.id }
30+
3531
if (data.status === ArticleStatus.PUBLISHED) {
36-
article.publishedAt = DateTime.now()
32+
payload.publishedAt = DateTime.now()
3733
}
38-
article.coverImage = data.coverImage || null
39-
article.canonicalUrl = data.canonicalUrl || null
40-
article.tags = data.tags || []
4134

42-
await article.generateSlug()
43-
await article.save()
35+
payload.slug = Article.generateSlug(data.title)
36+
37+
console.log('payload', payload)
38+
39+
const article = await Article.create(payload)
40+
41+
console.log('article', article)
4442

4543
return response.redirect().toRoute('articles.show', { slug: article.slug })
4644
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import BannedUser from '#models/banned_user'
2+
import Discussion from '#models/discussion'
3+
import Message from '#models/message'
4+
import type { HttpContext } from '@adonisjs/core/http'
5+
6+
export default class DiscussionsController {
7+
async index({ inertia, auth }: HttpContext) {
8+
await auth.check()
9+
// Liste des discussions publiques
10+
const discussions = await Discussion.query()
11+
.preload('author')
12+
.where('is_public', true)
13+
.orderBy('created_at', 'desc')
14+
return inertia.render('discussions/index', { discussions })
15+
}
16+
17+
async show({ inertia, params, auth }: HttpContext) {
18+
await auth.check()
19+
// Accès à une discussion (vérifier ban)
20+
const discussion = await Discussion.query()
21+
.where('id', params.id)
22+
.preload('author')
23+
.firstOrFail()
24+
// Vérifier si banni
25+
if (auth.user) {
26+
const ban = await BannedUser.query()
27+
.where('user_id', auth.user.id)
28+
.where('discussion_id', discussion.id)
29+
.first()
30+
if (ban) {
31+
return inertia.render('errors/banned', { reason: ban.reason })
32+
}
33+
}
34+
// Charger les messages (pagination à ajouter plus tard)
35+
const messages = await Message.query()
36+
.where('discussion_id', discussion.id)
37+
.orderBy('created_at', 'asc')
38+
.preload('sender')
39+
return inertia.render('discussions/show', { discussion, messages })
40+
}
41+
42+
async create({ inertia, auth }: HttpContext) {
43+
await auth.check()
44+
return inertia.render('discussions/create')
45+
}
46+
47+
async store({ request, auth, response }: HttpContext) {
48+
await auth.check()
49+
const { title, banner } = request.only(['title', 'banner'])
50+
const discussion = await Discussion.create({
51+
title,
52+
banner,
53+
authorId: auth.user!.id,
54+
isPublic: true,
55+
})
56+
return response.redirect().toRoute('discussions.show', { id: discussion.id })
57+
}
58+
59+
async destroy({ params, auth, response }: HttpContext) {
60+
// Suppression par admin
61+
// (Vérifier le rôle admin dans la logique réelle)
62+
const discussion = await Discussion.findOrFail(params.id)
63+
await discussion.delete()
64+
return response.redirect().toRoute('discussions.index')
65+
}
66+
67+
async ban({ params, request, auth, response }: HttpContext) {
68+
if (auth.user?.role !== 'ADMIN') {
69+
return response.unauthorized('Réservé admin')
70+
}
71+
const { userId, reason } = request.only(['userId', 'reason'])
72+
const discussionId = params.id
73+
// Vérifier si déjà banni
74+
const existing = await BannedUser.query()
75+
.where('user_id', userId)
76+
.where('discussion_id', discussionId)
77+
.first()
78+
if (existing) {
79+
return response.badRequest('Déjà banni')
80+
}
81+
await BannedUser.create({
82+
userId,
83+
discussionId,
84+
adminId: auth.user.id,
85+
reason: reason || null,
86+
})
87+
return response.ok({ success: true })
88+
}
89+
}

app/controllers/home_controller.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { HttpContext } from '@adonisjs/core/http'
1+
import { ArticleStatus } from '#enums/article_status'
22
import Article from '#models/article'
3+
import { HttpContext } from '@adonisjs/core/http'
34

45
export default class HomeController {
56
async index({ inertia }: HttpContext) {
6-
const articles = await Article.query().where('is_published', true).preload('author')
7+
const articles = await Article.query()
8+
.where('status', ArticleStatus.PUBLISHED)
9+
.preload('author')
710

811
return inertia.render('home', {
912
stats: {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import Message from '#models/message'
2+
import { getIO } from '#start/socket'
3+
import type { HttpContext } from '@adonisjs/core/http'
4+
5+
export default class MessagesController {
6+
async store({ request, auth, response }: HttpContext) {
7+
const { discussionId, type, content, fileUrl, tempId } = request.only([
8+
'discussionId',
9+
'type',
10+
'content',
11+
'fileUrl',
12+
'tempId',
13+
])
14+
// fileUrl est fourni par le frontend après upload via presigned URL
15+
const message = await Message.create({
16+
discussionId,
17+
senderId: auth.user!.id,
18+
type,
19+
content,
20+
fileUrl: fileUrl || null,
21+
status: 'normal',
22+
history: [],
23+
})
24+
await message.load('sender')
25+
const sender = message.sender?.toJSON()
26+
const messageWithTempId = { ...message.toJSON(), tempId, sender }
27+
getIO()?.to(`discussion:${discussionId}`).emit('message:new', messageWithTempId)
28+
return response.ok(messageWithTempId)
29+
}
30+
31+
async update({ params, request, auth, response }: HttpContext) {
32+
const message = await Message.findOrFail(params.id)
33+
if (message.senderId !== auth.user!.id) {
34+
return response.unauthorized('Non autorisé')
35+
}
36+
// Historique
37+
const oldVersion = {
38+
content: message.content,
39+
fileUrl: message.fileUrl,
40+
updatedAt: message.updatedAt,
41+
}
42+
const history = message.history || []
43+
history.push(oldVersion)
44+
message.merge({
45+
content: request.input('content'),
46+
status: 'edited',
47+
history,
48+
})
49+
await message.save()
50+
// Émettre l'événement socket.io
51+
getIO()?.to(`discussion:${message.discussionId}`).emit('message:edit', message)
52+
return response.ok(message)
53+
}
54+
55+
async destroy({ params, auth, response }: HttpContext) {
56+
const message = await Message.findOrFail(params.id)
57+
// Suppression par sender ou admin (à vérifier dans la logique réelle)
58+
if (message.senderId === auth.user!.id) {
59+
message.status = 'deleted'
60+
message.deletedBy = 'sender'
61+
} else if (auth.user!.role === 'ADMIN') {
62+
message.status = 'deleted'
63+
message.deletedBy = 'admin'
64+
} else {
65+
return response.unauthorized('Non autorisé')
66+
}
67+
await message.save()
68+
// Émettre l'événement socket.io
69+
getIO()?.to(`discussion:${message.discussionId}`).emit('message:delete', message)
70+
return response.ok(message)
71+
}
72+
73+
// Endpoint pour l'historique d'un message (admin)
74+
async history({ params, auth, response }: HttpContext) {
75+
if (auth.user?.role !== 'ADMIN') {
76+
return response.unauthorized('Réservé admin')
77+
}
78+
const message = await Message.findOrFail(params.id)
79+
return response.ok({ history: message.history || [] })
80+
}
81+
}

app/controllers/profile_controller.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import { HttpContext } from '@adonisjs/core/http'
2-
import User from '#models/user'
1+
import { ArticleStatus } from '#enums/article_status'
32
import Article from '#models/article'
3+
import User from '#models/user'
4+
import { HttpContext } from '@adonisjs/core/http'
45

56
export default class ProfileController {
67
async show({ params, inertia }: HttpContext) {
78
const user = await User.findByOrFail('username', params.username.replace('@', ''))
89
const articles = await Article.query()
910
.where('author_id', user.id)
10-
.where('is_published', true)
11+
.where('status', ArticleStatus.PUBLISHED)
1112
.orderBy('created_at', 'desc')
1213
.preload('author')
1314

app/models/article.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { DateTime } from 'luxon'
1+
import { ArticleStatus } from '#enums/article_status'
2+
import User from '#models/user'
3+
import string from '@adonisjs/core/helpers/string'
24
import { BaseModel, belongsTo, column } from '@adonisjs/lucid/orm'
35
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
4-
import string from '@adonisjs/core/helpers/string'
5-
import User from '#models/user'
6-
import { ArticleStatus } from '#enums/article_status'
6+
import { DateTime } from 'luxon'
77

88
export default class Article extends BaseModel {
99
@column({ isPrimary: true })
@@ -45,11 +45,14 @@ export default class Article extends BaseModel {
4545
@column()
4646
declare canonicalUrl: string | null
4747

48-
@column()
49-
declare tags: string[] | null
48+
@column({
49+
prepare: (value: string[] | string | null) => (Array.isArray(value) ? value.join(',') : value),
50+
consume: (value: string | null) => (value ? value.split(',') : []),
51+
})
52+
declare tags: string[]
5053

5154
// Generate slug before saving
52-
public async generateSlug() {
53-
this.slug = string.slug(this.title)
55+
public static generateSlug(title: string) {
56+
return string.slug(title)
5457
}
5558
}

app/models/banned_user.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Discussion from '#models/discussion'
2+
import User from '#models/user'
3+
import { BaseModel, belongsTo, column } from '@adonisjs/lucid/orm'
4+
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
5+
import { DateTime } from 'luxon'
6+
7+
export default class BannedUser extends BaseModel {
8+
@column({ isPrimary: true })
9+
declare id: number
10+
11+
@column()
12+
declare userId: number
13+
14+
@column()
15+
declare discussionId: number
16+
17+
@column()
18+
declare adminId: number
19+
20+
@column()
21+
declare reason: string | null
22+
23+
@belongsTo(() => User, { foreignKey: 'userId' })
24+
declare user: BelongsTo<typeof User>
25+
26+
@belongsTo(() => Discussion)
27+
declare discussion: BelongsTo<typeof Discussion>
28+
29+
@belongsTo(() => User, { foreignKey: 'adminId' })
30+
declare admin: BelongsTo<typeof User>
31+
32+
@column.dateTime({ autoCreate: true })
33+
declare createdAt: DateTime
34+
35+
@column.dateTime({ autoCreate: true, autoUpdate: true })
36+
declare updatedAt: DateTime
37+
}

app/models/discussion.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import BannedUser from '#models/banned_user'
2+
import Message from '#models/message'
3+
import User from '#models/user'
4+
import { BaseModel, belongsTo, column, hasMany } from '@adonisjs/lucid/orm'
5+
import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations'
6+
import { DateTime } from 'luxon'
7+
8+
export default class Discussion extends BaseModel {
9+
@column({ isPrimary: true })
10+
declare id: number
11+
12+
@column()
13+
declare title: string
14+
15+
@column()
16+
declare banner: string | null
17+
18+
@column()
19+
declare authorId: number
20+
21+
@column()
22+
declare isPublic: boolean
23+
24+
@belongsTo(() => User, { foreignKey: 'authorId' })
25+
declare author: BelongsTo<typeof User>
26+
27+
@hasMany(() => Message)
28+
declare messages: HasMany<typeof Message>
29+
30+
@hasMany(() => BannedUser)
31+
declare bans: HasMany<typeof BannedUser>
32+
33+
@column.dateTime({ autoCreate: true })
34+
declare createdAt: DateTime
35+
36+
@column.dateTime({ autoCreate: true, autoUpdate: true })
37+
declare updatedAt: DateTime
38+
}

0 commit comments

Comments
 (0)