@feelflow/ffid-sdk 0.1.0 → 0.3.0

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.
@@ -860,13 +860,539 @@ function FFIDSubscriptionBadge({
860
860
  );
861
861
  }
862
862
 
863
+ // src/announcements/ffid-announcements-client.ts
864
+ var API_PATH = "/api/v1/announcements";
865
+ var SDK_LOG_PREFIX2 = "[FFID Announcements SDK]";
866
+ var FFID_ANNOUNCEMENTS_ERROR_CODES = {
867
+ /** Network request failed (fetch threw) */
868
+ NETWORK_ERROR: "NETWORK_ERROR",
869
+ /** Failed to parse server response as JSON */
870
+ PARSE_ERROR: "PARSE_ERROR",
871
+ /** Server returned error without structured error body */
872
+ UNKNOWN_ERROR: "UNKNOWN_ERROR"
873
+ };
874
+ var quietLogger = {
875
+ debug: () => {
876
+ },
877
+ info: () => {
878
+ },
879
+ warn: () => {
880
+ },
881
+ error: (...args) => console.error(SDK_LOG_PREFIX2, ...args)
882
+ };
883
+ var consoleLogger2 = {
884
+ debug: (...args) => console.debug(SDK_LOG_PREFIX2, ...args),
885
+ info: (...args) => console.info(SDK_LOG_PREFIX2, ...args),
886
+ warn: (...args) => console.warn(SDK_LOG_PREFIX2, ...args),
887
+ error: (...args) => console.error(SDK_LOG_PREFIX2, ...args)
888
+ };
889
+ function createFFIDAnnouncementsClient(config = {}) {
890
+ const baseUrl = config.apiBaseUrl ?? DEFAULT_API_BASE_URL;
891
+ const logger = config.logger ?? (config.debug ? consoleLogger2 : quietLogger);
892
+ async function fetchApi(endpoint) {
893
+ const url = `${baseUrl}${endpoint}`;
894
+ logger.debug("Fetching:", url);
895
+ let response;
896
+ try {
897
+ response = await fetch(url, {
898
+ headers: {
899
+ Accept: "application/json"
900
+ }
901
+ });
902
+ } catch (error) {
903
+ logger.error("Network error:", { url, error });
904
+ return {
905
+ error: {
906
+ code: FFID_ANNOUNCEMENTS_ERROR_CODES.NETWORK_ERROR,
907
+ message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
908
+ }
909
+ };
910
+ }
911
+ let data;
912
+ try {
913
+ data = await response.json();
914
+ } catch (parseError) {
915
+ logger.error("Parse error:", { url, status: response.status, parseError });
916
+ return {
917
+ error: {
918
+ code: FFID_ANNOUNCEMENTS_ERROR_CODES.PARSE_ERROR,
919
+ message: `\u30B5\u30FC\u30D0\u30FC\u304B\u3089\u4E0D\u6B63\u306A\u30EC\u30B9\u30DD\u30F3\u30B9\u3092\u53D7\u4FE1\u3057\u307E\u3057\u305F (status: ${response.status})`
920
+ }
921
+ };
922
+ }
923
+ logger.debug("Response:", response.status, data);
924
+ if (!response.ok || !data.success) {
925
+ return {
926
+ error: data.error ?? {
927
+ code: FFID_ANNOUNCEMENTS_ERROR_CODES.UNKNOWN_ERROR,
928
+ message: "\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
929
+ }
930
+ };
931
+ }
932
+ if (data.data === void 0) {
933
+ return {
934
+ error: {
935
+ code: FFID_ANNOUNCEMENTS_ERROR_CODES.UNKNOWN_ERROR,
936
+ message: "\u30B5\u30FC\u30D0\u30FC\u304B\u3089\u30C7\u30FC\u30BF\u304C\u8FD4\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F"
937
+ }
938
+ };
939
+ }
940
+ return { data: data.data };
941
+ }
942
+ async function listAnnouncements(options = {}) {
943
+ const params = new URLSearchParams();
944
+ if (options.limit !== void 0) {
945
+ const limit = Math.max(0, Math.floor(options.limit));
946
+ params.set("limit", String(limit));
947
+ }
948
+ if (options.offset !== void 0) {
949
+ const offset = Math.max(0, Math.floor(options.offset));
950
+ params.set("offset", String(offset));
951
+ }
952
+ const query = params.toString();
953
+ const endpoint = query ? `${API_PATH}?${query}` : API_PATH;
954
+ return fetchApi(endpoint);
955
+ }
956
+ return {
957
+ /** List published announcements */
958
+ listAnnouncements,
959
+ /** Resolved logger instance */
960
+ logger,
961
+ /** API base URL */
962
+ baseUrl
963
+ };
964
+ }
965
+
966
+ // src/hooks/useFFIDAnnouncements.ts
967
+ var LAST_READ_STORAGE_KEY = "ffid-announcements-last-read";
968
+ function getStoredLastReadAt() {
969
+ try {
970
+ return localStorage.getItem(LAST_READ_STORAGE_KEY);
971
+ } catch {
972
+ return null;
973
+ }
974
+ }
975
+ function useFFIDAnnouncements(options = {}) {
976
+ const {
977
+ limit,
978
+ offset,
979
+ apiBaseUrl,
980
+ debug,
981
+ enabled = true
982
+ } = options;
983
+ const [announcements, setAnnouncements] = react.useState([]);
984
+ const [total, setTotal] = react.useState(0);
985
+ const [isLoading, setIsLoading] = react.useState(false);
986
+ const [error, setError] = react.useState(null);
987
+ const [lastReadAt, setLastReadAt] = react.useState(getStoredLastReadAt);
988
+ const resolvedBaseUrl = apiBaseUrl ?? DEFAULT_API_BASE_URL;
989
+ const client = react.useMemo(
990
+ () => createFFIDAnnouncementsClient({ apiBaseUrl: resolvedBaseUrl, debug }),
991
+ [resolvedBaseUrl, debug]
992
+ );
993
+ const unreadCount = react.useMemo(() => {
994
+ if (!lastReadAt) {
995
+ return announcements.length;
996
+ }
997
+ const lastReadTime = new Date(lastReadAt).getTime();
998
+ return announcements.filter(
999
+ (a) => a.publishedAt && new Date(a.publishedAt).getTime() > lastReadTime
1000
+ ).length;
1001
+ }, [announcements, lastReadAt]);
1002
+ const fetchAnnouncements = react.useCallback(async () => {
1003
+ setIsLoading(true);
1004
+ setError(null);
1005
+ try {
1006
+ const fetchOptions = {};
1007
+ if (limit !== void 0) fetchOptions.limit = limit;
1008
+ if (offset !== void 0) fetchOptions.offset = offset;
1009
+ const result = await client.listAnnouncements(fetchOptions);
1010
+ if (result.error) {
1011
+ setError(result.error);
1012
+ } else {
1013
+ setAnnouncements(result.data.announcements);
1014
+ setTotal(result.data.total);
1015
+ }
1016
+ } catch (err) {
1017
+ setError({
1018
+ code: FFID_ANNOUNCEMENTS_ERROR_CODES.UNKNOWN_ERROR,
1019
+ message: err instanceof Error ? err.message : "\u4E88\u671F\u3057\u306A\u3044\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
1020
+ });
1021
+ } finally {
1022
+ setIsLoading(false);
1023
+ }
1024
+ }, [client, limit, offset]);
1025
+ react.useEffect(() => {
1026
+ if (enabled) {
1027
+ void fetchAnnouncements();
1028
+ }
1029
+ }, [enabled, fetchAnnouncements]);
1030
+ const markAsRead = react.useCallback(() => {
1031
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1032
+ setLastReadAt(now);
1033
+ try {
1034
+ localStorage.setItem(LAST_READ_STORAGE_KEY, now);
1035
+ } catch {
1036
+ }
1037
+ }, []);
1038
+ return {
1039
+ announcements,
1040
+ total,
1041
+ unreadCount,
1042
+ isLoading,
1043
+ error,
1044
+ refetch: fetchAnnouncements,
1045
+ markAsRead
1046
+ };
1047
+ }
1048
+ var DEFAULT_ICON_SIZE = 20;
1049
+ var BADGE_SIZE = 18;
1050
+ var BADGE_FONT_SIZE = 11;
1051
+ var BADGE_OFFSET = -6;
1052
+ var BADGE_MAX_COUNT = 99;
1053
+ function BellIcon({ size, className }) {
1054
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1055
+ "svg",
1056
+ {
1057
+ xmlns: "http://www.w3.org/2000/svg",
1058
+ width: size,
1059
+ height: size,
1060
+ viewBox: "0 0 24 24",
1061
+ fill: "none",
1062
+ stroke: "currentColor",
1063
+ strokeWidth: 2,
1064
+ strokeLinecap: "round",
1065
+ strokeLinejoin: "round",
1066
+ className,
1067
+ "aria-hidden": "true",
1068
+ children: [
1069
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }),
1070
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })
1071
+ ]
1072
+ }
1073
+ );
1074
+ }
1075
+ function FFIDAnnouncementBadge({
1076
+ onClick,
1077
+ className = "",
1078
+ classNames = {},
1079
+ style,
1080
+ announcementOptions,
1081
+ iconSize = DEFAULT_ICON_SIZE,
1082
+ ariaLabel = "\u304A\u77E5\u3089\u305B"
1083
+ }) {
1084
+ const { unreadCount } = useFFIDAnnouncements(announcementOptions);
1085
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1086
+ "button",
1087
+ {
1088
+ type: "button",
1089
+ onClick,
1090
+ className: [className, classNames.button].filter(Boolean).join(" ") || void 0,
1091
+ style: {
1092
+ position: "relative",
1093
+ display: "inline-flex",
1094
+ alignItems: "center",
1095
+ justifyContent: "center",
1096
+ background: "none",
1097
+ border: "none",
1098
+ cursor: "pointer",
1099
+ padding: 4,
1100
+ borderRadius: 6,
1101
+ color: "inherit",
1102
+ ...style
1103
+ },
1104
+ "aria-label": `${ariaLabel}${unreadCount > 0 ? ` (${unreadCount}\u4EF6\u672A\u8AAD)` : ""}`,
1105
+ children: [
1106
+ /* @__PURE__ */ jsxRuntime.jsx(BellIcon, { size: iconSize, className: classNames.icon }),
1107
+ unreadCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1108
+ "span",
1109
+ {
1110
+ className: classNames.badge,
1111
+ style: {
1112
+ position: "absolute",
1113
+ top: BADGE_OFFSET,
1114
+ right: BADGE_OFFSET,
1115
+ display: "inline-flex",
1116
+ alignItems: "center",
1117
+ justifyContent: "center",
1118
+ minWidth: BADGE_SIZE,
1119
+ height: BADGE_SIZE,
1120
+ padding: "0 4px",
1121
+ borderRadius: 9999,
1122
+ fontSize: BADGE_FONT_SIZE,
1123
+ fontWeight: 600,
1124
+ lineHeight: 1,
1125
+ backgroundColor: "#ef4444",
1126
+ color: "white"
1127
+ },
1128
+ "aria-hidden": "true",
1129
+ children: unreadCount > BADGE_MAX_COUNT ? `${BADGE_MAX_COUNT}+` : unreadCount
1130
+ }
1131
+ )
1132
+ ]
1133
+ }
1134
+ );
1135
+ }
1136
+ function WrenchIcon() {
1137
+ return /* @__PURE__ */ jsxRuntime.jsx(
1138
+ "svg",
1139
+ {
1140
+ xmlns: "http://www.w3.org/2000/svg",
1141
+ width: 16,
1142
+ height: 16,
1143
+ viewBox: "0 0 24 24",
1144
+ fill: "none",
1145
+ stroke: "currentColor",
1146
+ strokeWidth: 2,
1147
+ strokeLinecap: "round",
1148
+ strokeLinejoin: "round",
1149
+ "aria-hidden": "true",
1150
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" })
1151
+ }
1152
+ );
1153
+ }
1154
+ function AlertTriangleIcon() {
1155
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1156
+ "svg",
1157
+ {
1158
+ xmlns: "http://www.w3.org/2000/svg",
1159
+ width: 16,
1160
+ height: 16,
1161
+ viewBox: "0 0 24 24",
1162
+ fill: "none",
1163
+ stroke: "currentColor",
1164
+ strokeWidth: 2,
1165
+ strokeLinecap: "round",
1166
+ strokeLinejoin: "round",
1167
+ "aria-hidden": "true",
1168
+ children: [
1169
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }),
1170
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 9v4" }),
1171
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 17h.01" })
1172
+ ]
1173
+ }
1174
+ );
1175
+ }
1176
+ function InfoIcon() {
1177
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1178
+ "svg",
1179
+ {
1180
+ xmlns: "http://www.w3.org/2000/svg",
1181
+ width: 16,
1182
+ height: 16,
1183
+ viewBox: "0 0 24 24",
1184
+ fill: "none",
1185
+ stroke: "currentColor",
1186
+ strokeWidth: 2,
1187
+ strokeLinecap: "round",
1188
+ strokeLinejoin: "round",
1189
+ "aria-hidden": "true",
1190
+ children: [
1191
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }),
1192
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 16v-4" }),
1193
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 8h.01" })
1194
+ ]
1195
+ }
1196
+ );
1197
+ }
1198
+ var TYPE_ICON_CONFIG = {
1199
+ maintenance: { icon: WrenchIcon, color: "#f59e0b" },
1200
+ incident: { icon: AlertTriangleIcon, color: "#ef4444" }
1201
+ };
1202
+ function getTypeIcon(type) {
1203
+ const category = type.split(".")[0];
1204
+ const config = TYPE_ICON_CONFIG[category];
1205
+ if (config) {
1206
+ return { icon: config.icon(), color: config.color };
1207
+ }
1208
+ return { icon: /* @__PURE__ */ jsxRuntime.jsx(InfoIcon, {}), color: "#6b7280" };
1209
+ }
1210
+ var DEFAULT_MAX_CONTENT_LINES = 2;
1211
+ function defaultFormatDate(dateStr) {
1212
+ try {
1213
+ const date = new Date(dateStr);
1214
+ return new Intl.DateTimeFormat("ja-JP", {
1215
+ year: "numeric",
1216
+ month: "2-digit",
1217
+ day: "2-digit",
1218
+ hour: "2-digit",
1219
+ minute: "2-digit"
1220
+ }).format(date);
1221
+ } catch {
1222
+ return dateStr;
1223
+ }
1224
+ }
1225
+ function FFIDAnnouncementList({
1226
+ announcements,
1227
+ isLoading = false,
1228
+ className = "",
1229
+ classNames = {},
1230
+ style,
1231
+ formatDate = defaultFormatDate,
1232
+ emptyMessage,
1233
+ loadingRender,
1234
+ renderItem,
1235
+ maxContentLines = DEFAULT_MAX_CONTENT_LINES
1236
+ }) {
1237
+ if (isLoading) {
1238
+ if (loadingRender) {
1239
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingRender });
1240
+ }
1241
+ return /* @__PURE__ */ jsxRuntime.jsx(
1242
+ "div",
1243
+ {
1244
+ className: [className, classNames.container].filter(Boolean).join(" ") || void 0,
1245
+ style: { padding: SPACING_MD, textAlign: "center", color: COLOR_TEXT_MUTED, ...style },
1246
+ children: "\u8AAD\u307F\u8FBC\u307F\u4E2D..."
1247
+ }
1248
+ );
1249
+ }
1250
+ if (announcements.length === 0) {
1251
+ return /* @__PURE__ */ jsxRuntime.jsx(
1252
+ "div",
1253
+ {
1254
+ className: [className, classNames.container, classNames.empty].filter(Boolean).join(" ") || void 0,
1255
+ style: { padding: SPACING_MD, textAlign: "center", color: COLOR_TEXT_MUTED, ...style },
1256
+ children: emptyMessage ?? "\u304A\u77E5\u3089\u305B\u306F\u3042\u308A\u307E\u305B\u3093"
1257
+ }
1258
+ );
1259
+ }
1260
+ return /* @__PURE__ */ jsxRuntime.jsx(
1261
+ "div",
1262
+ {
1263
+ className: [className, classNames.container].filter(Boolean).join(" ") || void 0,
1264
+ style: {
1265
+ display: "flex",
1266
+ flexDirection: "column",
1267
+ gap: 0,
1268
+ ...style
1269
+ },
1270
+ role: "list",
1271
+ "aria-label": "\u304A\u77E5\u3089\u305B\u4E00\u89A7",
1272
+ children: announcements.map((announcement) => {
1273
+ if (renderItem) {
1274
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "listitem", children: renderItem(announcement) }, announcement.id);
1275
+ }
1276
+ const { icon, color } = getTypeIcon(announcement.type);
1277
+ const displayDate = announcement.publishedAt ?? announcement.createdAt;
1278
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1279
+ "div",
1280
+ {
1281
+ className: classNames.item,
1282
+ style: {
1283
+ display: "flex",
1284
+ gap: SPACING_SM,
1285
+ padding: SPACING_MD,
1286
+ borderBottom: `1px solid ${COLOR_BORDER}`
1287
+ },
1288
+ role: "listitem",
1289
+ children: [
1290
+ /* @__PURE__ */ jsxRuntime.jsx(
1291
+ "div",
1292
+ {
1293
+ className: classNames.icon,
1294
+ style: {
1295
+ flexShrink: 0,
1296
+ color,
1297
+ marginTop: 2
1298
+ },
1299
+ children: icon
1300
+ }
1301
+ ),
1302
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
1303
+ /* @__PURE__ */ jsxRuntime.jsx(
1304
+ "div",
1305
+ {
1306
+ className: classNames.title,
1307
+ style: {
1308
+ fontSize: FONT_SIZE_SM,
1309
+ fontWeight: 600,
1310
+ lineHeight: 1.4,
1311
+ marginBottom: SPACING_XS
1312
+ },
1313
+ children: announcement.title
1314
+ }
1315
+ ),
1316
+ maxContentLines > 0 && announcement.content && /* @__PURE__ */ jsxRuntime.jsx(
1317
+ "div",
1318
+ {
1319
+ className: classNames.content,
1320
+ style: {
1321
+ fontSize: FONT_SIZE_XS,
1322
+ color: COLOR_TEXT_MUTED,
1323
+ lineHeight: 1.5,
1324
+ overflow: "hidden",
1325
+ display: "-webkit-box",
1326
+ WebkitLineClamp: maxContentLines,
1327
+ WebkitBoxOrient: "vertical",
1328
+ marginBottom: SPACING_XS
1329
+ },
1330
+ children: announcement.content
1331
+ }
1332
+ ),
1333
+ /* @__PURE__ */ jsxRuntime.jsxs(
1334
+ "div",
1335
+ {
1336
+ style: {
1337
+ display: "flex",
1338
+ flexWrap: "wrap",
1339
+ alignItems: "center",
1340
+ gap: SPACING_XS,
1341
+ marginTop: SPACING_XS
1342
+ },
1343
+ children: [
1344
+ /* @__PURE__ */ jsxRuntime.jsx(
1345
+ "span",
1346
+ {
1347
+ className: classNames.timestamp,
1348
+ style: {
1349
+ fontSize: FONT_SIZE_XS,
1350
+ color: COLOR_TEXT_MUTED
1351
+ },
1352
+ children: formatDate(displayDate)
1353
+ }
1354
+ ),
1355
+ announcement.affectedServices.length > 0 && announcement.affectedServices.map((service) => /* @__PURE__ */ jsxRuntime.jsx(
1356
+ "span",
1357
+ {
1358
+ className: classNames.serviceTag,
1359
+ style: {
1360
+ display: "inline-block",
1361
+ fontSize: FONT_SIZE_XS,
1362
+ padding: `1px ${SPACING_XS}px`,
1363
+ borderRadius: BORDER_RADIUS_SM,
1364
+ backgroundColor: "#f3f4f6",
1365
+ color: "#4b5563"
1366
+ },
1367
+ children: service
1368
+ },
1369
+ service
1370
+ ))
1371
+ ]
1372
+ }
1373
+ )
1374
+ ] })
1375
+ ]
1376
+ },
1377
+ announcement.id
1378
+ );
1379
+ })
1380
+ }
1381
+ );
1382
+ }
1383
+
863
1384
  exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;
1385
+ exports.FFIDAnnouncementBadge = FFIDAnnouncementBadge;
1386
+ exports.FFIDAnnouncementList = FFIDAnnouncementList;
864
1387
  exports.FFIDLoginButton = FFIDLoginButton;
865
1388
  exports.FFIDOrganizationSwitcher = FFIDOrganizationSwitcher;
866
1389
  exports.FFIDProvider = FFIDProvider;
867
1390
  exports.FFIDSubscriptionBadge = FFIDSubscriptionBadge;
868
1391
  exports.FFIDUserMenu = FFIDUserMenu;
1392
+ exports.FFID_ANNOUNCEMENTS_ERROR_CODES = FFID_ANNOUNCEMENTS_ERROR_CODES;
1393
+ exports.createFFIDAnnouncementsClient = createFFIDAnnouncementsClient;
869
1394
  exports.useFFID = useFFID;
1395
+ exports.useFFIDAnnouncements = useFFIDAnnouncements;
870
1396
  exports.useFFIDContext = useFFIDContext;
871
1397
  exports.useSubscription = useSubscription;
872
1398
  exports.withSubscription = withSubscription;
@@ -0,0 +1,4 @@
1
+ // src/constants.ts
2
+ var DEFAULT_API_BASE_URL = "https://id.feelflow.co.jp";
3
+
4
+ export { DEFAULT_API_BASE_URL };
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ // src/constants.ts
4
+ var DEFAULT_API_BASE_URL = "https://id.feelflow.co.jp";
5
+
6
+ exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;