@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,141 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect } from "react";
|
|
3
|
+
import { userEntitySchema } from "@plasius/entity-manager";
|
|
4
|
+
import { validateUserId } from "@plasius/schema";
|
|
5
|
+
import { useAuthorizedFetch } from "@plasius/auth";
|
|
6
|
+
import { createScopedStoreContext } from "@plasius/react-state";
|
|
7
|
+
export function ValidateUser(user) {
|
|
8
|
+
const validated = userEntitySchema.validate(user);
|
|
9
|
+
if (!validated.valid || !validated.value) {
|
|
10
|
+
throw new Error(`Invalid User Profile: ${validated.errors?.join(", ") ?? "unknown error"}`);
|
|
11
|
+
}
|
|
12
|
+
return validated.value;
|
|
13
|
+
}
|
|
14
|
+
const reducer = (state, action) => {
|
|
15
|
+
switch (action.type) {
|
|
16
|
+
case "setUser":
|
|
17
|
+
return { ...state, user: action.user };
|
|
18
|
+
case "setUserId":
|
|
19
|
+
return { ...state, userId: action.userId };
|
|
20
|
+
case "updateUser":
|
|
21
|
+
if (!state.user)
|
|
22
|
+
return state;
|
|
23
|
+
return {
|
|
24
|
+
...state,
|
|
25
|
+
user: {
|
|
26
|
+
...state.user,
|
|
27
|
+
...action.user,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
case "updateField":
|
|
31
|
+
if (!state.user)
|
|
32
|
+
return state;
|
|
33
|
+
return {
|
|
34
|
+
...state,
|
|
35
|
+
user: {
|
|
36
|
+
...state.user,
|
|
37
|
+
[action.payload.field]: action.payload.value,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
case "updateNameField":
|
|
41
|
+
if (!state.user || !state.user.name)
|
|
42
|
+
return state;
|
|
43
|
+
return {
|
|
44
|
+
...state,
|
|
45
|
+
user: {
|
|
46
|
+
...state.user,
|
|
47
|
+
name: {
|
|
48
|
+
...state.user.name,
|
|
49
|
+
[action.payload.field]: action.payload.value,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
case "updateAvatarField":
|
|
54
|
+
if (!state.user)
|
|
55
|
+
return state;
|
|
56
|
+
return {
|
|
57
|
+
...state,
|
|
58
|
+
user: {
|
|
59
|
+
...state.user,
|
|
60
|
+
avatar: {
|
|
61
|
+
...(state.user.avatar ?? {}),
|
|
62
|
+
[action.payload.field]: action.payload.value,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
default:
|
|
67
|
+
return state;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const initialState = {
|
|
71
|
+
user: undefined,
|
|
72
|
+
userId: null,
|
|
73
|
+
};
|
|
74
|
+
export const UserStore = createScopedStoreContext(reducer, initialState);
|
|
75
|
+
export const UserProvider = ({ children }) => {
|
|
76
|
+
return (_jsxs(UserStore.Provider, { children: [_jsx(UserInitializer, {}), children] }));
|
|
77
|
+
};
|
|
78
|
+
const UserInitializer = () => {
|
|
79
|
+
const authorizedFetch = useAuthorizedFetch();
|
|
80
|
+
const dispatch = UserStore.useDispatch();
|
|
81
|
+
const { userId, user } = UserStore.useStore();
|
|
82
|
+
const saveUser = useCallback(async () => {
|
|
83
|
+
if (!user)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
const validatedUser = ValidateUser(user);
|
|
87
|
+
const res = await authorizedFetch(`/users/${validatedUser.partitionKey}/update`, {
|
|
88
|
+
credentials: "include",
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify(validatedUser),
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
throw new Error(`Save failed with status ${res.status}`);
|
|
97
|
+
}
|
|
98
|
+
console.info("✅ User profile saved successfully.");
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
console.error("❌ Error saving user profile:", err);
|
|
102
|
+
}
|
|
103
|
+
}, [authorizedFetch, user]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!userId || !validateUserId(userId))
|
|
106
|
+
return;
|
|
107
|
+
const headers = { "Content-Type": "application/json" };
|
|
108
|
+
authorizedFetch(`/users/${userId}/get`, { credentials: "include" })
|
|
109
|
+
.then(async (res) => {
|
|
110
|
+
if (res.status === 404) {
|
|
111
|
+
const newUser = { id: userId };
|
|
112
|
+
return await authorizedFetch(`/users/${userId}/create`, {
|
|
113
|
+
credentials: "include",
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers,
|
|
116
|
+
body: JSON.stringify(newUser),
|
|
117
|
+
}).then(async (res) => {
|
|
118
|
+
if (!res.ok)
|
|
119
|
+
throw new Error(`Failed to load user (${res.status})`);
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
const validatedUser = ValidateUser(data);
|
|
122
|
+
dispatch({ type: "setUser", user: validatedUser });
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (!res.ok)
|
|
126
|
+
throw new Error(`Failed to load user (${res.status})`);
|
|
127
|
+
const data = await res.json();
|
|
128
|
+
const validatedUser = ValidateUser(data);
|
|
129
|
+
dispatch({ type: "setUser", user: validatedUser });
|
|
130
|
+
})
|
|
131
|
+
.catch((err) => {
|
|
132
|
+
console.warn("⚠️ Failed to load or create user profile:", err);
|
|
133
|
+
});
|
|
134
|
+
return () => {
|
|
135
|
+
saveUser().catch((err) => {
|
|
136
|
+
console.error("❌ Error saving during cleanup:", err);
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
}, [userId, authorizedFetch, dispatch, saveUser]);
|
|
140
|
+
return null;
|
|
141
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Settings.d.ts","sourceRoot":"","sources":["../../../src/Pages/Settings/Settings.tsx"],"names":[],"mappings":"AA0BA,wBAAgB,YAAY,4CAuM3B;AAED,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SettingsPage = SettingsPage;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const react_1 = require("react");
|
|
9
|
+
const entity_manager_1 = require("@plasius/entity-manager");
|
|
10
|
+
const auth_1 = require("@plasius/auth");
|
|
11
|
+
const translations_1 = require("@plasius/translations");
|
|
12
|
+
const UserProvider_js_1 = require("../../UserProvider.js");
|
|
13
|
+
const Settings_module_css_1 = __importDefault(require("./Settings.module.css"));
|
|
14
|
+
const getEmailPreferenceOptions = () => Object.entries(entity_manager_1.UserEmailPreferences).map(([key, value]) => ({
|
|
15
|
+
label: key.replace(/([a-z])([A-Z])/g, "$1 $2"), // Optional: format nicely
|
|
16
|
+
value,
|
|
17
|
+
}));
|
|
18
|
+
const getPreferredDisplayOrder = () => Object.entries(entity_manager_1.PreferredDisplayOrder).map(([key, value]) => ({
|
|
19
|
+
label: key.replace(/([a-z])([A-Z])/g, "$1 $2"), // Optional: format nicely
|
|
20
|
+
value,
|
|
21
|
+
}));
|
|
22
|
+
function SettingsPage() {
|
|
23
|
+
const authorizedFetch = (0, auth_1.useAuthorizedFetch)();
|
|
24
|
+
const { t } = (0, translations_1.useI18n)();
|
|
25
|
+
const { user } = UserProvider_js_1.UserStore.useStore();
|
|
26
|
+
const dispatch = UserProvider_js_1.UserStore.useDispatch();
|
|
27
|
+
(0, react_1.useEffect)(() => {
|
|
28
|
+
if (user) {
|
|
29
|
+
const res = entity_manager_1.userEntitySchema.validate(user);
|
|
30
|
+
if (!res.valid || res.errors?.length != 0) {
|
|
31
|
+
throw new Error(`Invalid user ${String(user)}. Errors: ${res.errors?.join("\n") ?? "unknown"}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}, [user]);
|
|
35
|
+
const handleChange = (e) => {
|
|
36
|
+
const { name, value } = e.target;
|
|
37
|
+
if (name.startsWith("name.")) {
|
|
38
|
+
const [, field] = name.split(".");
|
|
39
|
+
dispatch({ type: "updateNameField", payload: { field, value } });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
dispatch({ type: "updateField", payload: { field: name, value } });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const uploadAvatar = async (file) => {
|
|
46
|
+
const formData = new FormData();
|
|
47
|
+
formData.append("avatar", file); // "avatar" must match the expected server-side field
|
|
48
|
+
const response = await authorizedFetch(`/user/avatar`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
body: formData,
|
|
51
|
+
headers: {
|
|
52
|
+
// Do NOT set Content-Type explicitly; browser will set it with boundary
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error("Failed to upload avatar");
|
|
57
|
+
}
|
|
58
|
+
const data = await response.json();
|
|
59
|
+
return data; // e.g. { imageUrl: "...uploaded file url..." }
|
|
60
|
+
};
|
|
61
|
+
const handleAvatarChange = async (e) => {
|
|
62
|
+
const file = e.target.files?.[0];
|
|
63
|
+
if (!file)
|
|
64
|
+
return;
|
|
65
|
+
try {
|
|
66
|
+
const uploadedAvatar = await uploadAvatar(file);
|
|
67
|
+
const validatedAvatar = entity_manager_1.userAvatarSchema.validate(uploadedAvatar);
|
|
68
|
+
if (!validatedAvatar.valid || validatedAvatar.errors?.length) {
|
|
69
|
+
throw new Error((validatedAvatar.errors ?? []).map((e) => String(e)).join("; "));
|
|
70
|
+
}
|
|
71
|
+
const base = validatedAvatar.value;
|
|
72
|
+
const userAvatar = {
|
|
73
|
+
...base,
|
|
74
|
+
originalName: file.name,
|
|
75
|
+
};
|
|
76
|
+
dispatch({
|
|
77
|
+
type: "updateField",
|
|
78
|
+
payload: {
|
|
79
|
+
field: "avatar",
|
|
80
|
+
value: userAvatar,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error(err);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const handleSubmit = (e) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
const errors = entity_manager_1.userEntitySchema.validate(user);
|
|
91
|
+
if (errors) {
|
|
92
|
+
console.warn("Validation failed", errors);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.info("Saved:", user);
|
|
96
|
+
};
|
|
97
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: Settings_module_css_1.default.settingsContainer, children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: Settings_module_css_1.default.settingsForm, children: [(0, jsx_runtime_1.jsx)("h2", { children: t("profile_settings") }), (0, jsx_runtime_1.jsxs)("label", { className: Settings_module_css_1.default.label, children: [t("upload_avatar"), (0, jsx_runtime_1.jsx)("input", { type: "file", accept: "image/*", onChange: handleAvatarChange, className: Settings_module_css_1.default.input })] }), user?.avatar?.url && ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("img", { src: user?.avatar?.url, alt: t("avatar_preview"), className: Settings_module_css_1.default.avatarPreview }) })), (0, jsx_runtime_1.jsxs)("label", { className: Settings_module_css_1.default.label, children: [t("first_name"), (0, jsx_runtime_1.jsx)("input", { name: "name.firstName", value: user?.name?.firstName, onChange: handleChange, className: Settings_module_css_1.default.input })] }), (0, jsx_runtime_1.jsxs)("label", { className: Settings_module_css_1.default.label, children: [t("middle_name"), (0, jsx_runtime_1.jsx)("input", { name: "name.middleName", value: user?.name?.middleName, onChange: handleChange, className: Settings_module_css_1.default.input })] }), (0, jsx_runtime_1.jsxs)("label", { className: Settings_module_css_1.default.label, children: [t("last_name"), (0, jsx_runtime_1.jsx)("input", { name: "name.lastName", value: user?.name?.lastName, onChange: handleChange, className: Settings_module_css_1.default.input })] }), (0, jsx_runtime_1.jsxs)("label", { className: Settings_module_css_1.default.label, children: [t("display_name"), (0, jsx_runtime_1.jsx)("input", { name: "name.displayName", value: user?.name?.displayName, onChange: handleChange, className: Settings_module_css_1.default.input })] }), (0, jsx_runtime_1.jsxs)("label", { className: Settings_module_css_1.default.label, children: [t("preferred_name_display"), (0, jsx_runtime_1.jsxs)("select", { name: "displayPreferences", value: (user?.name).preferredDisplayOrder, onChange: handleChange, className: Settings_module_css_1.default.select, children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: t("select_preference") }), getPreferredDisplayOrder().map((opt) => ((0, jsx_runtime_1.jsx)("option", { value: opt.value, children: opt.label }, opt.value)))] })] }), (0, jsx_runtime_1.jsxs)("label", { className: Settings_module_css_1.default.label, children: [t("email"), (0, jsx_runtime_1.jsx)("input", { name: "email", value: user?.email, onChange: handleChange, className: Settings_module_css_1.default.input })] }), (0, jsx_runtime_1.jsxs)("label", { className: Settings_module_css_1.default.label, children: [t("email_preferences"), (0, jsx_runtime_1.jsxs)("select", { name: "emailPreferences", value: user?.emailPreferences, onChange: handleChange, className: Settings_module_css_1.default.select, children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: t("select_preference") }), getEmailPreferenceOptions().map((opt) => ((0, jsx_runtime_1.jsx)("option", { value: opt.value, children: opt.label }, opt.value)))] })] }), (0, jsx_runtime_1.jsx)("button", { type: "submit", className: Settings_module_css_1.default.submitButton, children: t("save_settings") })] }) }));
|
|
98
|
+
}
|
|
99
|
+
exports.default = SettingsPage;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.avatarPreview {
|
|
2
|
+
margin-top: 10px;
|
|
3
|
+
border-radius: 50%;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
width: 100px;
|
|
6
|
+
height: 100px;
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
background-color: var(--color-surface-muted);
|
|
11
|
+
box-shadow: var(--shadow-elevation-low);
|
|
12
|
+
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/Pages/Settings/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./Settings.js"), exports);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { IState } from "@plasius/react-state";
|
|
3
|
+
export type SettingsState = IState & Record<string, unknown>;
|
|
4
|
+
export type SettingsAction = {
|
|
5
|
+
type: "update";
|
|
6
|
+
key: string;
|
|
7
|
+
value: unknown;
|
|
8
|
+
} | {
|
|
9
|
+
type: "reset";
|
|
10
|
+
} | {
|
|
11
|
+
type: "load";
|
|
12
|
+
payload: Partial<SettingsState>;
|
|
13
|
+
};
|
|
14
|
+
export declare const SettingsStore: {
|
|
15
|
+
Context: React.Context<import("@plasius/react-state").Store<SettingsState, SettingsAction> | null>;
|
|
16
|
+
Provider: ({ children, initialState: override, }: {
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
initialState?: SettingsState | undefined;
|
|
19
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
useStore: () => SettingsState;
|
|
21
|
+
useDispatch: () => (action: SettingsAction) => void;
|
|
22
|
+
useSelector: <T>(selector: (state: SettingsState) => T, isEqual?: ((a: T, b: T) => boolean) | undefined) => T;
|
|
23
|
+
};
|
|
24
|
+
export declare const SettingsProvider: ({ children, configUrl, }: {
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
configUrl?: string;
|
|
27
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
//# sourceMappingURL=SettingsProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SettingsProvider.d.ts","sourceRoot":"","sources":["../src/SettingsProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAoB,MAAM,OAAO,CAAC;AAEzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAKnD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7D,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;CAAE,CAAC;AAoBtD,eAAO,MAAM,aAAa;;;;;;;;;CAGF,CAAC;AAEzB,eAAO,MAAM,gBAAgB,GAAI,0BAG9B;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,4CAMA,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SettingsProvider = exports.SettingsStore = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const PRIVATE_ID = Symbol("settings.id");
|
|
6
|
+
const PRIVATE_PARTITION_KEY = Symbol("settings.partitionKey");
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const react_state_1 = require("@plasius/react-state");
|
|
9
|
+
const auth_1 = require("@plasius/auth");
|
|
10
|
+
const react_query_1 = require("@plasius/react-query");
|
|
11
|
+
const initialState = {};
|
|
12
|
+
const reducer = (state, action) => {
|
|
13
|
+
switch (action.type) {
|
|
14
|
+
case "update":
|
|
15
|
+
return { ...state, [action.key]: action.value };
|
|
16
|
+
case "load":
|
|
17
|
+
return { ...state, ...action.payload };
|
|
18
|
+
case "reset":
|
|
19
|
+
return initialState;
|
|
20
|
+
default:
|
|
21
|
+
return state;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
exports.SettingsStore = (0, react_state_1.createScopedStoreContext)(reducer, initialState);
|
|
25
|
+
const SettingsProvider = ({ children, configUrl = "/settings", }) => {
|
|
26
|
+
return ((0, jsx_runtime_1.jsx)(exports.SettingsStore.Provider, { children: (0, jsx_runtime_1.jsx)(SettingsInitializer, { configUrl: configUrl, children: children }) }));
|
|
27
|
+
};
|
|
28
|
+
exports.SettingsProvider = SettingsProvider;
|
|
29
|
+
const SettingsInitializer = ({ children, configUrl, }) => {
|
|
30
|
+
const authorizedFetch = (0, auth_1.useAuthorizedFetch)();
|
|
31
|
+
const dispatch = exports.SettingsStore.useDispatch();
|
|
32
|
+
const state = exports.SettingsStore.useStore();
|
|
33
|
+
const queryClient = (0, react_query_1.useQueryClient)();
|
|
34
|
+
const toSettingsEntity = (state) => {
|
|
35
|
+
const hidden = state;
|
|
36
|
+
return {
|
|
37
|
+
id: hidden[PRIVATE_ID] ?? "",
|
|
38
|
+
partitionKey: hidden[PRIVATE_PARTITION_KEY] ?? "",
|
|
39
|
+
settings: state,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const fromSettingsEntity = (entity) => {
|
|
43
|
+
const result = {
|
|
44
|
+
...entity.settings,
|
|
45
|
+
};
|
|
46
|
+
Object.defineProperty(result, PRIVATE_ID, {
|
|
47
|
+
value: entity.id,
|
|
48
|
+
enumerable: false,
|
|
49
|
+
writable: false,
|
|
50
|
+
});
|
|
51
|
+
Object.defineProperty(result, PRIVATE_PARTITION_KEY, {
|
|
52
|
+
value: entity.partitionKey,
|
|
53
|
+
enumerable: false,
|
|
54
|
+
writable: false,
|
|
55
|
+
});
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
const { data } = (0, react_query_1.useQuery)("settings", async () => {
|
|
59
|
+
const res = await authorizedFetch(configUrl);
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
throw new Error(`Load failed with status ${res.status}`);
|
|
62
|
+
return (await res.json());
|
|
63
|
+
});
|
|
64
|
+
(0, react_1.useEffect)(() => {
|
|
65
|
+
if (data) {
|
|
66
|
+
dispatch({ type: "load", payload: fromSettingsEntity(data) });
|
|
67
|
+
}
|
|
68
|
+
}, [data]);
|
|
69
|
+
const saveMutation = (0, react_query_1.useMutation)(async () => {
|
|
70
|
+
const res = await authorizedFetch(configUrl, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify(toSettingsEntity(state)),
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new Error(`Save failed with status ${res.status}`);
|
|
79
|
+
}, {
|
|
80
|
+
onSuccess: () => {
|
|
81
|
+
console.info("✅ Settings saved successfully.");
|
|
82
|
+
queryClient.invalidateQuery("settings", async () => {
|
|
83
|
+
const res = await authorizedFetch(configUrl);
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
throw new Error(`Load failed with status ${res.status}`);
|
|
86
|
+
const entity = (await res.json());
|
|
87
|
+
return entity;
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
onError: (err) => {
|
|
91
|
+
console.error("❌ Error saving settings:", err);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
(0, react_1.useEffect)(() => {
|
|
95
|
+
return () => {
|
|
96
|
+
void saveMutation.mutate();
|
|
97
|
+
};
|
|
98
|
+
}, [saveMutation]);
|
|
99
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children });
|
|
100
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type UserEntity } from "@plasius/entity-manager";
|
|
3
|
+
import { type IState } from "@plasius/react-state";
|
|
4
|
+
export declare function ValidateUser(user: UserEntity): UserEntity;
|
|
5
|
+
interface UserState extends IState {
|
|
6
|
+
user?: UserEntity;
|
|
7
|
+
userId: string | null;
|
|
8
|
+
}
|
|
9
|
+
type UserAction = {
|
|
10
|
+
type: "setUser";
|
|
11
|
+
user: UserEntity;
|
|
12
|
+
} | {
|
|
13
|
+
type: "setUserId";
|
|
14
|
+
userId: string | null;
|
|
15
|
+
} | {
|
|
16
|
+
type: "updateUser";
|
|
17
|
+
user: Partial<UserEntity>;
|
|
18
|
+
} | {
|
|
19
|
+
type: "updateField";
|
|
20
|
+
payload: {
|
|
21
|
+
field: string;
|
|
22
|
+
value: unknown;
|
|
23
|
+
};
|
|
24
|
+
} | {
|
|
25
|
+
type: "updateNameField";
|
|
26
|
+
payload: {
|
|
27
|
+
field: string;
|
|
28
|
+
value: unknown;
|
|
29
|
+
};
|
|
30
|
+
} | {
|
|
31
|
+
type: "updateAvatarField";
|
|
32
|
+
payload: {
|
|
33
|
+
field: string;
|
|
34
|
+
value: unknown;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export declare const UserStore: {
|
|
38
|
+
Context: React.Context<import("@plasius/react-state").Store<UserState, UserAction> | null>;
|
|
39
|
+
Provider: ({ children, initialState: override, }: {
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
initialState?: UserState | undefined;
|
|
42
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
useStore: () => UserState;
|
|
44
|
+
useDispatch: () => (action: UserAction) => void;
|
|
45
|
+
useSelector: <T>(selector: (state: UserState) => T, isEqual?: ((a: T, b: T) => boolean) | undefined) => T;
|
|
46
|
+
};
|
|
47
|
+
export declare const UserProvider: ({ children }: {
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=UserProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserProvider.d.ts","sourceRoot":"","sources":["../src/UserProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAiC,MAAM,OAAO,CAAC;AACtD,OAAO,EAAyB,KAAK,UAAU,EAAoB,MAAM,yBAAyB,CAAC;AAGnG,OAAO,EAA4B,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAE7E,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAON,UAAU,CAChD;AAED,UAAU,SAAU,SAAQ,MAAM;IAChC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,KAAK,UAAU,GACX;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GACvE;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,CAAC;AA4D9E,eAAO,MAAM,SAAS;;;;;;;;;CAAkD,CAAC;AAEzE,eAAO,MAAM,YAAY,GAAI,cAAc;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,4CAOvE,CAAC"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UserProvider = exports.UserStore = void 0;
|
|
4
|
+
exports.ValidateUser = ValidateUser;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const entity_manager_1 = require("@plasius/entity-manager");
|
|
8
|
+
const schema_1 = require("@plasius/schema");
|
|
9
|
+
const auth_1 = require("@plasius/auth");
|
|
10
|
+
const react_state_1 = require("@plasius/react-state");
|
|
11
|
+
function ValidateUser(user) {
|
|
12
|
+
const validated = entity_manager_1.userEntitySchema.validate(user);
|
|
13
|
+
if (!validated.valid || !validated.value) {
|
|
14
|
+
throw new Error(`Invalid User Profile: ${validated.errors?.join(", ") ?? "unknown error"}`);
|
|
15
|
+
}
|
|
16
|
+
return validated.value;
|
|
17
|
+
}
|
|
18
|
+
const reducer = (state, action) => {
|
|
19
|
+
switch (action.type) {
|
|
20
|
+
case "setUser":
|
|
21
|
+
return { ...state, user: action.user };
|
|
22
|
+
case "setUserId":
|
|
23
|
+
return { ...state, userId: action.userId };
|
|
24
|
+
case "updateUser":
|
|
25
|
+
if (!state.user)
|
|
26
|
+
return state;
|
|
27
|
+
return {
|
|
28
|
+
...state,
|
|
29
|
+
user: {
|
|
30
|
+
...state.user,
|
|
31
|
+
...action.user,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
case "updateField":
|
|
35
|
+
if (!state.user)
|
|
36
|
+
return state;
|
|
37
|
+
return {
|
|
38
|
+
...state,
|
|
39
|
+
user: {
|
|
40
|
+
...state.user,
|
|
41
|
+
[action.payload.field]: action.payload.value,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
case "updateNameField":
|
|
45
|
+
if (!state.user || !state.user.name)
|
|
46
|
+
return state;
|
|
47
|
+
return {
|
|
48
|
+
...state,
|
|
49
|
+
user: {
|
|
50
|
+
...state.user,
|
|
51
|
+
name: {
|
|
52
|
+
...state.user.name,
|
|
53
|
+
[action.payload.field]: action.payload.value,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
case "updateAvatarField":
|
|
58
|
+
if (!state.user)
|
|
59
|
+
return state;
|
|
60
|
+
return {
|
|
61
|
+
...state,
|
|
62
|
+
user: {
|
|
63
|
+
...state.user,
|
|
64
|
+
avatar: {
|
|
65
|
+
...(state.user.avatar ?? {}),
|
|
66
|
+
[action.payload.field]: action.payload.value,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
default:
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const initialState = {
|
|
75
|
+
user: undefined,
|
|
76
|
+
userId: null,
|
|
77
|
+
};
|
|
78
|
+
exports.UserStore = (0, react_state_1.createScopedStoreContext)(reducer, initialState);
|
|
79
|
+
const UserProvider = ({ children }) => {
|
|
80
|
+
return ((0, jsx_runtime_1.jsxs)(exports.UserStore.Provider, { children: [(0, jsx_runtime_1.jsx)(UserInitializer, {}), children] }));
|
|
81
|
+
};
|
|
82
|
+
exports.UserProvider = UserProvider;
|
|
83
|
+
const UserInitializer = () => {
|
|
84
|
+
const authorizedFetch = (0, auth_1.useAuthorizedFetch)();
|
|
85
|
+
const dispatch = exports.UserStore.useDispatch();
|
|
86
|
+
const { userId, user } = exports.UserStore.useStore();
|
|
87
|
+
const saveUser = (0, react_1.useCallback)(async () => {
|
|
88
|
+
if (!user)
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
const validatedUser = ValidateUser(user);
|
|
92
|
+
const res = await authorizedFetch(`/users/${validatedUser.partitionKey}/update`, {
|
|
93
|
+
credentials: "include",
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify(validatedUser),
|
|
99
|
+
});
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
throw new Error(`Save failed with status ${res.status}`);
|
|
102
|
+
}
|
|
103
|
+
console.info("✅ User profile saved successfully.");
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error("❌ Error saving user profile:", err);
|
|
107
|
+
}
|
|
108
|
+
}, [authorizedFetch, user]);
|
|
109
|
+
(0, react_1.useEffect)(() => {
|
|
110
|
+
if (!userId || !(0, schema_1.validateUserId)(userId))
|
|
111
|
+
return;
|
|
112
|
+
const headers = { "Content-Type": "application/json" };
|
|
113
|
+
authorizedFetch(`/users/${userId}/get`, { credentials: "include" })
|
|
114
|
+
.then(async (res) => {
|
|
115
|
+
if (res.status === 404) {
|
|
116
|
+
const newUser = { id: userId };
|
|
117
|
+
return await authorizedFetch(`/users/${userId}/create`, {
|
|
118
|
+
credentials: "include",
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers,
|
|
121
|
+
body: JSON.stringify(newUser),
|
|
122
|
+
}).then(async (res) => {
|
|
123
|
+
if (!res.ok)
|
|
124
|
+
throw new Error(`Failed to load user (${res.status})`);
|
|
125
|
+
const data = await res.json();
|
|
126
|
+
const validatedUser = ValidateUser(data);
|
|
127
|
+
dispatch({ type: "setUser", user: validatedUser });
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (!res.ok)
|
|
131
|
+
throw new Error(`Failed to load user (${res.status})`);
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
const validatedUser = ValidateUser(data);
|
|
134
|
+
dispatch({ type: "setUser", user: validatedUser });
|
|
135
|
+
})
|
|
136
|
+
.catch((err) => {
|
|
137
|
+
console.warn("⚠️ Failed to load or create user profile:", err);
|
|
138
|
+
});
|
|
139
|
+
return () => {
|
|
140
|
+
saveUser().catch((err) => {
|
|
141
|
+
console.error("❌ Error saving during cleanup:", err);
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
}, [userId, authorizedFetch, dispatch, saveUser]);
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC"}
|