@djangocfg/layouts 2.1.263 → 2.1.264

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.263",
3
+ "version": "2.1.264",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -74,14 +74,14 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
77
- "@djangocfg/api": "^2.1.263",
78
- "@djangocfg/centrifugo": "^2.1.263",
79
- "@djangocfg/i18n": "^2.1.263",
80
- "@djangocfg/monitor": "^2.1.263",
81
- "@djangocfg/debuger": "^2.1.263",
82
- "@djangocfg/ui-core": "^2.1.263",
83
- "@djangocfg/ui-nextjs": "^2.1.263",
84
- "@djangocfg/ui-tools": "^2.1.263",
77
+ "@djangocfg/api": "^2.1.264",
78
+ "@djangocfg/centrifugo": "^2.1.264",
79
+ "@djangocfg/i18n": "^2.1.264",
80
+ "@djangocfg/monitor": "^2.1.264",
81
+ "@djangocfg/debuger": "^2.1.264",
82
+ "@djangocfg/ui-core": "^2.1.264",
83
+ "@djangocfg/ui-nextjs": "^2.1.264",
84
+ "@djangocfg/ui-tools": "^2.1.264",
85
85
  "@hookform/resolvers": "^5.2.2",
86
86
  "consola": "^3.4.2",
87
87
  "lucide-react": "^0.545.0",
@@ -109,15 +109,15 @@
109
109
  "uuid": "^11.1.0"
110
110
  },
111
111
  "devDependencies": {
112
- "@djangocfg/api": "^2.1.263",
113
- "@djangocfg/i18n": "^2.1.263",
114
- "@djangocfg/centrifugo": "^2.1.263",
115
- "@djangocfg/monitor": "^2.1.263",
116
- "@djangocfg/debuger": "^2.1.263",
117
- "@djangocfg/typescript-config": "^2.1.263",
118
- "@djangocfg/ui-core": "^2.1.263",
119
- "@djangocfg/ui-nextjs": "^2.1.263",
120
- "@djangocfg/ui-tools": "^2.1.263",
112
+ "@djangocfg/api": "^2.1.264",
113
+ "@djangocfg/i18n": "^2.1.264",
114
+ "@djangocfg/centrifugo": "^2.1.264",
115
+ "@djangocfg/monitor": "^2.1.264",
116
+ "@djangocfg/debuger": "^2.1.264",
117
+ "@djangocfg/typescript-config": "^2.1.264",
118
+ "@djangocfg/ui-core": "^2.1.264",
119
+ "@djangocfg/ui-nextjs": "^2.1.264",
120
+ "@djangocfg/ui-tools": "^2.1.264",
121
121
  "@types/node": "^24.7.2",
122
122
  "@types/react": "^19.1.0",
123
123
  "@types/react-dom": "^19.1.0",
@@ -5,7 +5,7 @@ import moment from 'moment';
5
5
  import React, { useEffect, useMemo, useState } from 'react';
6
6
 
7
7
  import { useAppT } from '@djangocfg/i18n';
8
- import { toast } from '@djangocfg/ui-core/hooks';
8
+ import { toast, useImageLoader } from '@djangocfg/ui-core/hooks';
9
9
 
10
10
  import { useAuth } from '@djangocfg/api/auth';
11
11
  import {
@@ -38,6 +38,80 @@ interface ProfileLayoutProps {
38
38
  enableDeleteAccount?: boolean;
39
39
  }
40
40
 
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+ // Avatar with image check
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+
45
+ function ProfileAvatar({
46
+ src,
47
+ initials,
48
+ isUploading,
49
+ onChange,
50
+ label,
51
+ }: {
52
+ src?: string | null;
53
+ initials: string;
54
+ isUploading: boolean;
55
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
56
+ label: string;
57
+ }) {
58
+ const { isLoading, isLoaded } = useImageLoader(src ?? undefined);
59
+
60
+ return (
61
+ <div className="relative group mb-4">
62
+ <Avatar className="w-28 h-28 text-3xl">
63
+ {/* Skeleton while image is loading */}
64
+ {src && isLoading && (
65
+ <div className="w-full h-full rounded-full bg-muted animate-pulse" />
66
+ )}
67
+
68
+ {/* Image — show only when loaded successfully */}
69
+ {src && (
70
+ <img
71
+ src={src}
72
+ alt=""
73
+ className={cn(
74
+ 'w-full h-full object-cover rounded-full transition-opacity duration-300',
75
+ isLoaded ? 'opacity-100' : 'opacity-0 absolute inset-0',
76
+ )}
77
+ />
78
+ )}
79
+
80
+ {/* Fallback — show when no src or image failed */}
81
+ {(!src || (!isLoading && !isLoaded)) && (
82
+ <AvatarFallback className="bg-muted text-muted-foreground text-2xl font-medium">
83
+ {initials}
84
+ </AvatarFallback>
85
+ )}
86
+ </Avatar>
87
+
88
+ {/* Upload overlay */}
89
+ <label
90
+ className={cn(
91
+ 'absolute inset-0 rounded-full flex items-center justify-center cursor-pointer',
92
+ 'bg-black/0 group-hover:bg-black/40 transition-colors',
93
+ )}
94
+ title={label}
95
+ >
96
+ <div className="opacity-0 group-hover:opacity-100 transition-opacity">
97
+ {isUploading ? (
98
+ <div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
99
+ ) : (
100
+ <Camera className="w-6 h-6 text-white drop-shadow" />
101
+ )}
102
+ </div>
103
+ <input
104
+ type="file"
105
+ accept="image/*"
106
+ onChange={onChange}
107
+ className="hidden"
108
+ disabled={isUploading}
109
+ />
110
+ </label>
111
+ </div>
112
+ );
113
+ }
114
+
41
115
  // ─────────────────────────────────────────────────────────────────────────────
42
116
  // Profile Content
43
117
  // ─────────────────────────────────────────────────────────────────────────────
@@ -150,39 +224,13 @@ const ProfileContent = ({
150
224
  <div className="container mx-auto px-4 py-12 max-w-md">
151
225
  {/* Avatar + header */}
152
226
  <div className="flex flex-col items-center mb-12">
153
- <div className="relative group mb-4">
154
- <Avatar className="w-28 h-28 text-3xl">
155
- {user.avatar ? (
156
- <img src={user.avatar} alt="" className="w-full h-full object-cover" />
157
- ) : (
158
- <AvatarFallback className="bg-muted text-muted-foreground text-2xl font-medium">
159
- {getInitials(user.display_username || user.email || '')}
160
- </AvatarFallback>
161
- )}
162
- </Avatar>
163
- <label
164
- className={cn(
165
- 'absolute inset-0 rounded-full flex items-center justify-center cursor-pointer',
166
- 'bg-black/0 group-hover:bg-black/40 transition-colors'
167
- )}
168
- title={labels.changeAvatar}
169
- >
170
- <div className="opacity-0 group-hover:opacity-100 transition-opacity">
171
- {isUploading ? (
172
- <div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
173
- ) : (
174
- <Camera className="w-6 h-6 text-white" />
175
- )}
176
- </div>
177
- <input
178
- type="file"
179
- accept="image/*"
180
- onChange={handleAvatarChange}
181
- className="hidden"
182
- disabled={isUploading}
183
- />
184
- </label>
185
- </div>
227
+ <ProfileAvatar
228
+ src={user.avatar}
229
+ initials={getInitials(user.display_username || user.email || '')}
230
+ isUploading={isUploading}
231
+ onChange={handleAvatarChange}
232
+ label={labels.changeAvatar}
233
+ />
186
234
 
187
235
  <h1 className="text-2xl font-semibold tracking-tight">{displayName}</h1>
188
236
  <p className="text-[15px] text-muted-foreground mt-1">{user.email}</p>