Skip to content

Commit 1180f7f

Browse files
committed
Optimistic UI profile modal
1 parent dd7c8f6 commit 1180f7f

File tree

5 files changed

+92
-4
lines changed

5 files changed

+92
-4
lines changed

app/controllers/application_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
class ApplicationController < ActionController::Base
22
include Pagy::Method
3+
include UserProfileable
34

45
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
56
allow_browser versions: :modern
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module UserProfileable
2+
extend ActiveSupport::Concern
3+
4+
included do
5+
inertia_share do
6+
{
7+
user_profile_id: params[:user_profile_id]&.to_i,
8+
user_profile: load_user_profile
9+
}.compact
10+
end
11+
end
12+
13+
private
14+
15+
def load_user_profile
16+
return nil if params[:user_profile_id].blank?
17+
sleep 1
18+
19+
User.find_by(id: params[:user_profile_id])
20+
&.as_json(only: [ :username, :email, :about_me, :id, :topics_count, :messages_count ])
21+
end
22+
end

app/frontend/components/Avatar.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
1+
import { Link, router } from '@inertiajs/react'
12
import { User } from '../types'
23

34
const Avatar = ({ user }: { user: User }) => {
4-
const { username } = user;
5+
const { username, id } = user;
56

67
if (!user) {
78
return null;
89
}
910

11+
const handleClick = (url: string) => {
12+
router.push({
13+
url,
14+
props: (currentProps) => ({ ...currentProps, user_profile_id: id }),
15+
preserveState: true,
16+
preserveScroll: true,
17+
})
18+
}
19+
1020
return (
11-
<div
21+
<Link
22+
href=""
23+
data={{ user_profile_id: id }}
24+
only={['user_profile', 'user_profile_id']}
1225
className="w-8 h-8 rounded-full bg-sky-100 flex items-center justify-center text-sky-600 text-xs font-medium"
1326
title={username}
27+
// preserveUrl // If we don't want shareable links
28+
onBefore={(e) => handleClick(e.url.toString())}
1429
>
1530
{username.charAt(0).toUpperCase()}
16-
</div>
31+
</Link>
1732
)
1833
}
1934

20-
export default Avatar
35+
export default Avatar
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Modal from './Modal'
2+
import {router, usePage} from '@inertiajs/react'
3+
4+
import LoadingSpinner from '@/components/LoadingSpinner';
5+
import UserProfile from '@/components/UserProfile';
6+
import type {User} from '@/types'
7+
8+
export default function UserProfileModal() {
9+
const {props: {user_profile_id: userProfileId, user_profile: userProfile}} = usePage<{
10+
user_profile_id: number,
11+
user_profile: User
12+
}>()
13+
14+
const isLoading = userProfileId && !userProfile;
15+
16+
const handleClose = () => {
17+
const pathname = window.location.pathname;
18+
const params = new URLSearchParams(window.location.search);
19+
params.delete('user_profile_id');
20+
21+
const queryString = params.toString();
22+
const url = queryString ? `${pathname}?${queryString}` : pathname;
23+
24+
router.visit(url, {
25+
preserveScroll: true,
26+
preserveState: true,
27+
onBefore: (e) => {
28+
router.push({
29+
url: e.url.toString(),
30+
props: (currentProps) => ({...currentProps, user_profile_id: null}),
31+
preserveState: true,
32+
preserveScroll: true,
33+
})
34+
}
35+
})
36+
}
37+
38+
return (
39+
<Modal isOpen={!!userProfileId} onClose={handleClose} title='User Profile'>
40+
{isLoading ? (
41+
<LoadingSpinner/>
42+
) : (
43+
<UserProfile user={userProfile} onClose={handleClose}/>
44+
)}
45+
</Modal>
46+
)
47+
}

app/frontend/layouts/AppLayout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
MagnifyingGlassIcon,
77
ChevronDownIcon
88
} from '@heroicons/react/24/outline'
9+
910
import { User } from '@/types'
1011
import Sidebar from "@/components/Sidebar";
12+
import UserProfileModal from '../components/UserProfileModal'
1113

1214
interface AppLayoutProps {
1315
children: ReactNode
@@ -95,6 +97,7 @@ export default function AppLayout({ children }: AppLayoutProps) {
9597
{children}
9698
</main>
9799
</div>
100+
<UserProfileModal />
98101
</div>
99102
)
100103
}

0 commit comments

Comments
 (0)