@plasius/profile 1.0.4
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/CHANGELOG.md +66 -0
- package/CODE_OF_CONDUCT.md +79 -0
- package/CONTRIBUTORS.md +27 -0
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/SECURITY.md +17 -0
- package/dist/Pages/Settings/Settings.d.ts +3 -0
- package/dist/Pages/Settings/Settings.d.ts.map +1 -0
- package/dist/Pages/Settings/Settings.js +93 -0
- package/dist/Pages/Settings/Settings.module.css +13 -0
- package/dist/Pages/Settings/index.d.ts +2 -0
- package/dist/Pages/Settings/index.d.ts.map +1 -0
- package/dist/Pages/Settings/index.js +1 -0
- package/dist/SettingsProvider.d.ts +28 -0
- package/dist/SettingsProvider.d.ts.map +1 -0
- package/dist/SettingsProvider.js +96 -0
- package/dist/UserProvider.d.ts +51 -0
- package/dist/UserProvider.d.ts.map +1 -0
- package/dist/UserProvider.js +141 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist-cjs/Pages/Settings/Settings.d.ts +3 -0
- package/dist-cjs/Pages/Settings/Settings.d.ts.map +1 -0
- package/dist-cjs/Pages/Settings/Settings.js +99 -0
- package/dist-cjs/Pages/Settings/Settings.module.css +13 -0
- package/dist-cjs/Pages/Settings/index.d.ts +2 -0
- package/dist-cjs/Pages/Settings/index.d.ts.map +1 -0
- package/dist-cjs/Pages/Settings/index.js +17 -0
- package/dist-cjs/SettingsProvider.d.ts +28 -0
- package/dist-cjs/SettingsProvider.d.ts.map +1 -0
- package/dist-cjs/SettingsProvider.js +100 -0
- package/dist-cjs/UserProvider.d.ts +51 -0
- package/dist-cjs/UserProvider.d.ts.map +1 -0
- package/dist-cjs/UserProvider.js +146 -0
- package/dist-cjs/index.d.ts +4 -0
- package/dist-cjs/index.d.ts.map +1 -0
- package/dist-cjs/index.js +19 -0
- package/docs/adrs/adr-0001-profile-package-scope.md +21 -0
- package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
- package/docs/adrs/adr-template.md +35 -0
- package/legal/CLA-REGISTRY.csv +1 -0
- package/legal/CLA.md +22 -0
- package/legal/CORPORATE_CLA.md +57 -0
- package/legal/INDIVIDUAL_CLA.md +91 -0
- package/package.json +109 -0
- package/src/Pages/Settings/Settings.module.css +13 -0
- package/src/Pages/Settings/Settings.tsx +228 -0
- package/src/Pages/Settings/index.ts +1 -0
- package/src/SettingsProvider.tsx +146 -0
- package/src/UserProvider.tsx +167 -0
- package/src/declarations.d.ts +5 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const PRIVATE_ID = Symbol("settings.id");
|
|
2
|
+
const PRIVATE_PARTITION_KEY = Symbol("settings.partitionKey");
|
|
3
|
+
import React, { useEffect } from "react";
|
|
4
|
+
import { createScopedStoreContext } from "@plasius/react-state";
|
|
5
|
+
import type { IState } from "@plasius/react-state";
|
|
6
|
+
import { useAuthorizedFetch } from "@plasius/auth";
|
|
7
|
+
import { useQuery, useMutation, useQueryClient } from "@plasius/react-query";
|
|
8
|
+
import type { SettingsEntity } from "@plasius/entity-manager";
|
|
9
|
+
|
|
10
|
+
export type SettingsState = IState & Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
export type SettingsAction =
|
|
13
|
+
| { type: "update"; key: string; value: unknown }
|
|
14
|
+
| { type: "reset" }
|
|
15
|
+
| { type: "load"; payload: Partial<SettingsState> };
|
|
16
|
+
|
|
17
|
+
const initialState: SettingsState = {};
|
|
18
|
+
|
|
19
|
+
const reducer = (
|
|
20
|
+
state: SettingsState,
|
|
21
|
+
action: SettingsAction
|
|
22
|
+
): SettingsState => {
|
|
23
|
+
switch (action.type) {
|
|
24
|
+
case "update":
|
|
25
|
+
return { ...state, [action.key]: action.value };
|
|
26
|
+
case "load":
|
|
27
|
+
return { ...state, ...action.payload };
|
|
28
|
+
case "reset":
|
|
29
|
+
return initialState;
|
|
30
|
+
default:
|
|
31
|
+
return state;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const SettingsStore = createScopedStoreContext<
|
|
36
|
+
SettingsState,
|
|
37
|
+
SettingsAction
|
|
38
|
+
>(reducer, initialState);
|
|
39
|
+
|
|
40
|
+
export const SettingsProvider = ({
|
|
41
|
+
children,
|
|
42
|
+
configUrl = "/settings",
|
|
43
|
+
}: {
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
configUrl?: string;
|
|
46
|
+
}) => {
|
|
47
|
+
return (
|
|
48
|
+
<SettingsStore.Provider>
|
|
49
|
+
<SettingsInitializer configUrl={configUrl}>{children}</SettingsInitializer>
|
|
50
|
+
</SettingsStore.Provider>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const SettingsInitializer = ({
|
|
55
|
+
children,
|
|
56
|
+
configUrl,
|
|
57
|
+
}: {
|
|
58
|
+
children: React.ReactNode;
|
|
59
|
+
configUrl: string;
|
|
60
|
+
}) => {
|
|
61
|
+
const authorizedFetch = useAuthorizedFetch();
|
|
62
|
+
const dispatch = SettingsStore.useDispatch();
|
|
63
|
+
const state = SettingsStore.useStore();
|
|
64
|
+
const queryClient = useQueryClient();
|
|
65
|
+
|
|
66
|
+
type HiddenSettingsKeys = {
|
|
67
|
+
[K in typeof PRIVATE_ID | typeof PRIVATE_PARTITION_KEY]?: string;
|
|
68
|
+
};
|
|
69
|
+
const toSettingsEntity = (state: SettingsState): SettingsEntity => {
|
|
70
|
+
const hidden = state as SettingsState & HiddenSettingsKeys;
|
|
71
|
+
return {
|
|
72
|
+
id: hidden[PRIVATE_ID] ?? "",
|
|
73
|
+
partitionKey: hidden[PRIVATE_PARTITION_KEY] ?? "",
|
|
74
|
+
settings: state,
|
|
75
|
+
} as unknown as SettingsEntity;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const fromSettingsEntity = (
|
|
79
|
+
entity: SettingsEntity
|
|
80
|
+
): Partial<SettingsState> => {
|
|
81
|
+
const result: Partial<SettingsState> = {
|
|
82
|
+
...(entity.settings as Record<string, unknown>),
|
|
83
|
+
};
|
|
84
|
+
Object.defineProperty(result, PRIVATE_ID, {
|
|
85
|
+
value: entity.id,
|
|
86
|
+
enumerable: false,
|
|
87
|
+
writable: false,
|
|
88
|
+
});
|
|
89
|
+
Object.defineProperty(result, PRIVATE_PARTITION_KEY, {
|
|
90
|
+
value: entity.partitionKey,
|
|
91
|
+
enumerable: false,
|
|
92
|
+
writable: false,
|
|
93
|
+
});
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const { data } = useQuery<SettingsEntity>(
|
|
98
|
+
"settings",
|
|
99
|
+
async (): Promise<SettingsEntity> => {
|
|
100
|
+
const res = await authorizedFetch(configUrl);
|
|
101
|
+
if (!res.ok) throw new Error(`Load failed with status ${res.status}`);
|
|
102
|
+
return (await res.json()) as SettingsEntity;
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (data) {
|
|
108
|
+
dispatch({ type: "load", payload: fromSettingsEntity(data) });
|
|
109
|
+
}
|
|
110
|
+
}, [data]);
|
|
111
|
+
|
|
112
|
+
const saveMutation = useMutation(
|
|
113
|
+
async () => {
|
|
114
|
+
const res = await authorizedFetch(configUrl, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: {
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify(toSettingsEntity(state)),
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) throw new Error(`Save failed with status ${res.status}`);
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
onSuccess: () => {
|
|
125
|
+
console.info("✅ Settings saved successfully.");
|
|
126
|
+
queryClient.invalidateQuery("settings", async () => {
|
|
127
|
+
const res = await authorizedFetch(configUrl);
|
|
128
|
+
if (!res.ok) throw new Error(`Load failed with status ${res.status}`);
|
|
129
|
+
const entity = (await res.json()) as SettingsEntity;
|
|
130
|
+
return entity;
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
onError: (err: unknown) => {
|
|
134
|
+
console.error("❌ Error saving settings:", err);
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
return () => {
|
|
141
|
+
void saveMutation.mutate();
|
|
142
|
+
};
|
|
143
|
+
}, [saveMutation]);
|
|
144
|
+
|
|
145
|
+
return <>{children}</>;
|
|
146
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import React, { useCallback, useEffect } from "react";
|
|
2
|
+
import { type UserAvatarEntity, type UserEntity, userEntitySchema } from "@plasius/entity-manager";
|
|
3
|
+
import { validateUserId } from "@plasius/schema";
|
|
4
|
+
import { useAuthorizedFetch } from "@plasius/auth";
|
|
5
|
+
import { createScopedStoreContext, type IState } from "@plasius/react-state";
|
|
6
|
+
|
|
7
|
+
export function ValidateUser(user: UserEntity) {
|
|
8
|
+
const validated = userEntitySchema.validate(user);
|
|
9
|
+
if (!validated.valid || !validated.value) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`Invalid User Profile: ${validated.errors?.join(", ") ?? "unknown error"}`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return validated.value as unknown as UserEntity;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UserState extends IState {
|
|
18
|
+
user?: UserEntity;
|
|
19
|
+
userId: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type UserAction =
|
|
23
|
+
| { type: "setUser"; user: UserEntity }
|
|
24
|
+
| { type: "setUserId"; userId: string | null }
|
|
25
|
+
| { type: "updateUser"; user: Partial<UserEntity> }
|
|
26
|
+
| { type: "updateField"; payload: { field: string; value: unknown } }
|
|
27
|
+
| { type: "updateNameField"; payload: { field: string; value: unknown } }
|
|
28
|
+
| { type: "updateAvatarField"; payload: { field: string; value: unknown } };
|
|
29
|
+
|
|
30
|
+
const reducer = (state: UserState, action: UserAction): UserState => {
|
|
31
|
+
switch (action.type) {
|
|
32
|
+
case "setUser":
|
|
33
|
+
return { ...state, user: action.user };
|
|
34
|
+
case "setUserId":
|
|
35
|
+
return { ...state, userId: action.userId };
|
|
36
|
+
case "updateUser":
|
|
37
|
+
if (!state.user) return state;
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
user: {
|
|
41
|
+
...state.user,
|
|
42
|
+
...action.user,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
case "updateField":
|
|
46
|
+
if (!state.user) return state;
|
|
47
|
+
return {
|
|
48
|
+
...state,
|
|
49
|
+
user: {
|
|
50
|
+
...state.user,
|
|
51
|
+
[action.payload.field]: action.payload.value,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
case "updateNameField":
|
|
55
|
+
if (!state.user || !state.user.name) return state;
|
|
56
|
+
return {
|
|
57
|
+
...state,
|
|
58
|
+
user: {
|
|
59
|
+
...state.user,
|
|
60
|
+
name: {
|
|
61
|
+
...state.user.name,
|
|
62
|
+
[action.payload.field]: action.payload.value,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
case "updateAvatarField":
|
|
67
|
+
if (!state.user) return state;
|
|
68
|
+
return {
|
|
69
|
+
...state,
|
|
70
|
+
user: {
|
|
71
|
+
...state.user,
|
|
72
|
+
avatar: {
|
|
73
|
+
...(state.user.avatar ?? {}),
|
|
74
|
+
[action.payload.field]: action.payload.value,
|
|
75
|
+
} as UserAvatarEntity,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
default:
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const initialState: UserState = {
|
|
84
|
+
user: undefined,
|
|
85
|
+
userId: null,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const UserStore = createScopedStoreContext(reducer, initialState);
|
|
89
|
+
|
|
90
|
+
export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
|
91
|
+
return (
|
|
92
|
+
<UserStore.Provider>
|
|
93
|
+
<UserInitializer />
|
|
94
|
+
{children}
|
|
95
|
+
</UserStore.Provider>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const UserInitializer = () => {
|
|
100
|
+
const authorizedFetch = useAuthorizedFetch();
|
|
101
|
+
const dispatch = UserStore.useDispatch();
|
|
102
|
+
const { userId, user } = UserStore.useStore();
|
|
103
|
+
|
|
104
|
+
const saveUser = useCallback(async () => {
|
|
105
|
+
if (!user) return;
|
|
106
|
+
try {
|
|
107
|
+
const validatedUser = ValidateUser(user);
|
|
108
|
+
const res = await authorizedFetch(
|
|
109
|
+
`/users/${validatedUser.partitionKey}/update`,
|
|
110
|
+
{
|
|
111
|
+
credentials: "include",
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify(validatedUser),
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
throw new Error(`Save failed with status ${res.status}`);
|
|
121
|
+
}
|
|
122
|
+
console.info("✅ User profile saved successfully.");
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error("❌ Error saving user profile:", err);
|
|
125
|
+
}
|
|
126
|
+
}, [authorizedFetch, user]);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!userId || !validateUserId(userId)) return;
|
|
130
|
+
|
|
131
|
+
const headers = { "Content-Type": "application/json" };
|
|
132
|
+
|
|
133
|
+
authorizedFetch(`/users/${userId}/get`, { credentials: "include" })
|
|
134
|
+
.then(async (res) => {
|
|
135
|
+
if (res.status === 404) {
|
|
136
|
+
const newUser: Partial<UserEntity> = { id: userId };
|
|
137
|
+
return await authorizedFetch(`/users/${userId}/create`, {
|
|
138
|
+
credentials: "include",
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers,
|
|
141
|
+
body: JSON.stringify(newUser),
|
|
142
|
+
}).then(async (res) => {
|
|
143
|
+
if (!res.ok) throw new Error(`Failed to load user (${res.status})`);
|
|
144
|
+
const data: unknown = await res.json();
|
|
145
|
+
const validatedUser = ValidateUser(data as UserEntity);
|
|
146
|
+
dispatch({ type: "setUser", user: validatedUser });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!res.ok) throw new Error(`Failed to load user (${res.status})`);
|
|
151
|
+
const data: unknown = await res.json();
|
|
152
|
+
const validatedUser = ValidateUser(data as UserEntity);
|
|
153
|
+
dispatch({ type: "setUser", user: validatedUser });
|
|
154
|
+
})
|
|
155
|
+
.catch((err) => {
|
|
156
|
+
console.warn("⚠️ Failed to load or create user profile:", err);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
saveUser().catch((err) => {
|
|
161
|
+
console.error("❌ Error saving during cleanup:", err);
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
}, [userId, authorizedFetch, dispatch, saveUser]);
|
|
165
|
+
|
|
166
|
+
return null;
|
|
167
|
+
};
|
package/src/index.ts
ADDED