Skip to content

Commit 118d33d

Browse files
committed
✨ Add Profile and Dashboard Features
1 parent 50430c2 commit 118d33d

11 files changed

Lines changed: 545 additions & 16 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { HttpContext } from '@adonisjs/core/http'
2+
import User from '#models/user'
3+
import Article from '#models/article'
4+
5+
export default class ProfileController {
6+
async show({ params, inertia }: HttpContext) {
7+
const user = await User.findByOrFail('username', params.username.replace('@', ''))
8+
const articles = await Article.query()
9+
.where('author_id', user.id)
10+
.where('is_published', true)
11+
.orderBy('created_at', 'desc')
12+
.preload('author')
13+
14+
return inertia.render('profile/show', {
15+
profile: {
16+
...user.serialize(),
17+
articles: articles,
18+
},
19+
})
20+
}
21+
}

app/middleware/guest_middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default class GuestMiddleware {
1313
/**
1414
* The URL to redirect to when user is logged-in
1515
*/
16-
redirectTo = '/'
16+
redirectTo = '/dashboard'
1717

1818
async handle(
1919
ctx: HttpContext,

app/models/user.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { DateTime } from 'luxon'
2-
import { BaseModel, beforeSave, column } from '@adonisjs/lucid/orm'
2+
import { BaseModel, beforeSave, column, hasMany } from '@adonisjs/lucid/orm'
33
import { compose } from '@adonisjs/core/helpers'
44
import hash from '@adonisjs/core/services/hash'
55
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'
6+
import type { HasMany } from '@adonisjs/lucid/types/relations'
7+
import Article from '#models/article'
68

79
const AuthFinder = withAuthFinder(() => hash.use('scrypt'), {
810
uids: ['email'],
@@ -52,6 +54,9 @@ export default class User extends compose(BaseModel, AuthFinder) {
5254
@column.dateTime({ autoCreate: true, autoUpdate: true })
5355
declare updatedAt: DateTime
5456

57+
@hasMany(() => Article)
58+
declare articles: HasMany<typeof Article>
59+
5560
@beforeSave()
5661
static async hashPassword(user: User) {
5762
if (user.$dirty.password) {

config/inertia.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ const inertiaConfig = defineConfig({
1111
* Data that should be shared with all rendered pages
1212
*/
1313
sharedData: {
14-
user: (ctx) => ctx.auth.user,
14+
auth: (ctx) => {
15+
return {
16+
user: ctx.auth.user,
17+
}
18+
},
1519
errors: (ctx) => ctx.inertia.always(() => ctx.session?.flashMessages.get('errors')),
1620
},
1721

inertia/components/navbar.tsx

Lines changed: 104 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
1-
import { Link } from '@inertiajs/react'
1+
import { Link, usePage } from '@inertiajs/react'
2+
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
3+
import { Fragment } from 'react'
4+
5+
interface User {
6+
name: string
7+
email: string
8+
username: string
9+
avatar: string | null
10+
}
11+
12+
interface PageProps {
13+
auth: {
14+
user: User | null
15+
}
16+
[key: string]: any
17+
}
218

319
export default function Navbar() {
20+
const { auth } = usePage<PageProps>().props
421
return (
522
<nav className="bg-white border-b border-gray-100">
623
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -38,17 +55,92 @@ export default function Navbar() {
3855

3956
{/* Right Side */}
4057
<div className="hidden sm:flex sm:items-center sm:ml-6">
41-
<div className="flex items-center space-x-4">
42-
<Link href="/login" className="text-sm text-gray-500 hover:text-gray-900">
43-
Se connecter
44-
</Link>
45-
<Link
46-
href="/register"
47-
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
48-
>
49-
Créer un compte
50-
</Link>
51-
</div>
58+
{auth && auth.user && auth.user.username ? (
59+
<Menu as="div" className="relative ml-3">
60+
<MenuButton className="flex items-center space-x-3 text-sm focus:outline-none">
61+
<img
62+
className="h-8 w-8 rounded-full"
63+
src={auth.user.avatar || `https://ui-avatars.com/api/?name=${auth.user.name}`}
64+
alt={auth.user.name}
65+
/>
66+
<span className="text-gray-700">{auth.user.name}</span>
67+
</MenuButton>
68+
69+
<Transition
70+
as={Fragment}
71+
enter="transition ease-out duration-100"
72+
enterFrom="transform opacity-0 scale-95"
73+
enterTo="transform opacity-100 scale-100"
74+
leave="transition ease-in duration-75"
75+
leaveFrom="transform opacity-100 scale-100"
76+
leaveTo="transform opacity-0 scale-95"
77+
>
78+
<MenuItems className="absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
79+
{/* Tableau de bord */}
80+
<MenuItem>
81+
{({ focus }) => (
82+
<Link
83+
href="/dashboard"
84+
className={`${focus ? 'bg-gray-100' : ''} block px-4 py-2 text-sm text-gray-700`}
85+
>
86+
Dashboard
87+
</Link>
88+
)}
89+
</MenuItem>
90+
<MenuItem>
91+
{({ focus }) => (
92+
<Link
93+
href={`/@${auth.user?.username}`}
94+
className={`${
95+
focus ? 'bg-gray-100' : ''
96+
} block px-4 py-2 text-sm text-gray-700`}
97+
>
98+
Profile
99+
</Link>
100+
)}
101+
</MenuItem>
102+
<MenuItem>
103+
{({ focus }) => (
104+
<Link
105+
href="/settings"
106+
className={`${
107+
focus ? 'bg-gray-100' : ''
108+
} block px-4 py-2 text-sm text-gray-700`}
109+
>
110+
Settings
111+
</Link>
112+
)}
113+
</MenuItem>
114+
<MenuItem>
115+
{({ focus }) => (
116+
<Link
117+
href="/logout"
118+
method="post"
119+
as="button"
120+
className={`${
121+
focus ? 'bg-gray-100' : ''
122+
} block w-full px-4 py-2 text-left text-sm text-gray-700`}
123+
>
124+
Logout
125+
</Link>
126+
)}
127+
</MenuItem>
128+
</MenuItems>
129+
</Transition>
130+
</Menu>
131+
) : (
132+
<div className="flex items-center space-x-4">
133+
<Link href="/login" className="text-sm text-gray-500 hover:text-gray-900">
134+
Se connecter
135+
</Link>
136+
<Link
137+
href="/register"
138+
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
139+
>
140+
Créer un compte
141+
</Link>
142+
</div>
143+
)}
52144
</div>
53145
</div>
54146
</div>

inertia/layouts/dashboard.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { PropsWithChildren } from 'react'
2+
import Footer from '../components/footer'
3+
import Navbar from '~/components/navbar'
4+
5+
export default function DashboardLayout({ children }: PropsWithChildren) {
6+
return (
7+
<div className="min-h-screen bg-gray-100">
8+
<Navbar />
9+
<main className="container mx-auto">
10+
<div className="py-6">
11+
<div className="px-4 sm:px-6 md:px-0">{children}</div>
12+
</div>
13+
</main>
14+
<Footer />
15+
</div>
16+
)
17+
}

inertia/pages/dashboard/index.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Head, Link } from '@inertiajs/react'
2+
import DashboardLayout from '../../layouts/dashboard'
3+
4+
interface DashboardProps {
5+
stats: {
6+
articles: number
7+
discussions: number
8+
questions: number
9+
}
10+
}
11+
12+
export default function Dashboard({ stats }: DashboardProps) {
13+
return (
14+
<DashboardLayout>
15+
<Head title="Dashboard - JavaScript Cameroun" />
16+
17+
<div className="space-y-6">
18+
{/* Stats */}
19+
<div className="grid grid-cols-1 gap-5 sm:grid-cols-3">
20+
<div className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
21+
<dt className="truncate text-sm font-medium text-gray-500">Total Articles</dt>
22+
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900">
23+
{stats.articles}
24+
</dd>
25+
</div>
26+
<div className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
27+
<dt className="truncate text-sm font-medium text-gray-500">Total Discussions</dt>
28+
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900">
29+
{stats.discussions}
30+
</dd>
31+
</div>
32+
<div className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
33+
<dt className="truncate text-sm font-medium text-gray-500">Total Questions</dt>
34+
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900">
35+
{stats.questions}
36+
</dd>
37+
</div>
38+
</div>
39+
<div className="flex justify-end">
40+
<Link
41+
href="/articles/create"
42+
className="btn btn-primary bg-blue-500 text-white px-4 py-2 rounded-md"
43+
>
44+
Create Article
45+
</Link>
46+
</div>
47+
48+
{/* Recent Activity */}
49+
<div className="overflow-hidden rounded-lg bg-white shadow">
50+
<div className="p-6">
51+
<h3 className="text-base font-semibold leading-6 text-gray-900">Recent Activity</h3>
52+
<div className="mt-6">
53+
<div className="text-center">
54+
<p className="text-sm text-gray-500">No recent activity</p>
55+
</div>
56+
</div>
57+
</div>
58+
</div>
59+
</div>
60+
</DashboardLayout>
61+
)
62+
}

inertia/pages/profile/show.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Head } from '@inertiajs/react'
2+
import Navbar from '../../components/navbar'
3+
import Footer from '../../components/footer'
4+
import ArticlesList from '../../components/articles/list'
5+
6+
interface ProfileProps {
7+
profile: {
8+
username: string
9+
name: string
10+
email: string
11+
avatar: string | null
12+
articles: Array<{
13+
id: number
14+
title: string
15+
slug: string
16+
excerpt: string
17+
author: {
18+
name: string
19+
username: string
20+
}
21+
publishedAt: string
22+
}>
23+
}
24+
}
25+
26+
export default function Profile({ profile }: ProfileProps) {
27+
return (
28+
<>
29+
<Head title={`${profile.name} (@${profile.username}) - JavaScript Cameroun`} />
30+
<Navbar />
31+
32+
<div className="bg-white">
33+
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12">
34+
{/* Profile Header */}
35+
<div className="sm:flex sm:items-center sm:space-x-5">
36+
<div className="flex">
37+
<img
38+
className="h-20 w-20 rounded-full"
39+
src={profile.avatar || `https://ui-avatars.com/api/?name=${profile.name}`}
40+
alt={profile.name}
41+
/>
42+
</div>
43+
<div className="mt-4 sm:mt-0 min-w-0 flex-1">
44+
<p className="text-sm font-medium text-gray-600">@{profile.username}</p>
45+
<h1 className="text-2xl font-bold text-gray-900 truncate">{profile.name}</h1>
46+
</div>
47+
</div>
48+
49+
{/* Tabs */}
50+
<div className="mt-6 sm:mt-2 2xl:mt-5">
51+
<div className="border-b border-gray-200">
52+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
53+
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
54+
<button className="border-indigo-500 text-indigo-600 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
55+
Articles
56+
</button>
57+
<button className="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
58+
Discussions
59+
</button>
60+
<button className="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
61+
Questions
62+
</button>
63+
</nav>
64+
</div>
65+
</div>
66+
</div>
67+
68+
{/* Content */}
69+
<div className="mt-6">
70+
{profile.articles.length > 0 ? (
71+
<ArticlesList
72+
articles={{
73+
data: profile.articles,
74+
meta: {
75+
total: profile.articles.length,
76+
per_page: 10,
77+
current_page: 1,
78+
last_page: 1,
79+
},
80+
}}
81+
/>
82+
) : (
83+
<div className="text-center py-12">
84+
<h3 className="mt-2 text-sm font-medium text-gray-900">Aucun article</h3>
85+
<p className="mt-1 text-sm text-gray-500">
86+
{profile.name} n'a pas encore publié d'articles.
87+
</p>
88+
</div>
89+
)}
90+
</div>
91+
</div>
92+
</div>
93+
94+
<Footer />
95+
</>
96+
)
97+
}

0 commit comments

Comments
 (0)