@orsetra/shared-ui 1.0.4 → 1.0.6

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.
@@ -1,8 +1,189 @@
1
- // Placeholder - will be implemented with actual toast logic
2
- export function useToast() {
3
- return {
4
- toast: (options: any) => {
5
- console.log('Toast:', options);
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ const TOAST_LIMIT = 1
6
+ const TOAST_REMOVE_DELAY = 1000000
7
+
8
+ type ToasterToast = {
9
+ id: string
10
+ title?: React.ReactNode
11
+ description?: React.ReactNode
12
+ action?: React.ReactElement
13
+ open?: boolean
14
+ onOpenChange?: (open: boolean) => void
15
+ variant?: "default" | "destructive"
16
+ }
17
+
18
+ const actionTypes = {
19
+ ADD_TOAST: "ADD_TOAST",
20
+ UPDATE_TOAST: "UPDATE_TOAST",
21
+ DISMISS_TOAST: "DISMISS_TOAST",
22
+ REMOVE_TOAST: "REMOVE_TOAST",
23
+ } as const
24
+
25
+ let count = 0
26
+
27
+ function genId() {
28
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
29
+ return count.toString()
30
+ }
31
+
32
+ type ActionType = typeof actionTypes
33
+
34
+ type Action =
35
+ | {
36
+ type: ActionType["ADD_TOAST"]
37
+ toast: ToasterToast
38
+ }
39
+ | {
40
+ type: ActionType["UPDATE_TOAST"]
41
+ toast: Partial<ToasterToast>
42
+ }
43
+ | {
44
+ type: ActionType["DISMISS_TOAST"]
45
+ toastId?: ToasterToast["id"]
46
+ }
47
+ | {
48
+ type: ActionType["REMOVE_TOAST"]
49
+ toastId?: ToasterToast["id"]
50
+ }
51
+
52
+ interface State {
53
+ toasts: ToasterToast[]
54
+ }
55
+
56
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
57
+
58
+ const addToRemoveQueue = (toastId: string) => {
59
+ if (toastTimeouts.has(toastId)) {
60
+ return
61
+ }
62
+
63
+ const timeout = setTimeout(() => {
64
+ toastTimeouts.delete(toastId)
65
+ dispatch({
66
+ type: "REMOVE_TOAST",
67
+ toastId: toastId,
68
+ })
69
+ }, TOAST_REMOVE_DELAY)
70
+
71
+ toastTimeouts.set(toastId, timeout)
72
+ }
73
+
74
+ export const reducer = (state: State, action: Action): State => {
75
+ switch (action.type) {
76
+ case "ADD_TOAST":
77
+ return {
78
+ ...state,
79
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80
+ }
81
+
82
+ case "UPDATE_TOAST":
83
+ return {
84
+ ...state,
85
+ toasts: state.toasts.map((t) =>
86
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
87
+ ),
88
+ }
89
+
90
+ case "DISMISS_TOAST": {
91
+ const { toastId } = action
92
+
93
+ if (toastId) {
94
+ addToRemoveQueue(toastId)
95
+ } else {
96
+ state.toasts.forEach((toast) => {
97
+ addToRemoveQueue(toast.id)
98
+ })
99
+ }
100
+
101
+ return {
102
+ ...state,
103
+ toasts: state.toasts.map((t) =>
104
+ t.id === toastId || toastId === undefined
105
+ ? {
106
+ ...t,
107
+ open: false,
108
+ }
109
+ : t
110
+ ),
111
+ }
112
+ }
113
+ case "REMOVE_TOAST":
114
+ if (action.toastId === undefined) {
115
+ return {
116
+ ...state,
117
+ toasts: [],
118
+ }
119
+ }
120
+ return {
121
+ ...state,
122
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
123
+ }
124
+ }
125
+ }
126
+
127
+ const listeners: Array<(state: State) => void> = []
128
+
129
+ let memoryState: State = { toasts: [] }
130
+
131
+ function dispatch(action: Action) {
132
+ memoryState = reducer(memoryState, action)
133
+ listeners.forEach((listener) => {
134
+ listener(memoryState)
135
+ })
136
+ }
137
+
138
+ type Toast = Omit<ToasterToast, "id">
139
+
140
+ function toast({ ...props }: Toast) {
141
+ const id = genId()
142
+
143
+ const update = (props: ToasterToast) =>
144
+ dispatch({
145
+ type: "UPDATE_TOAST",
146
+ toast: { ...props, id },
147
+ })
148
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
149
+
150
+ dispatch({
151
+ type: "ADD_TOAST",
152
+ toast: {
153
+ ...props,
154
+ id,
155
+ open: true,
156
+ onOpenChange: (open) => {
157
+ if (!open) dismiss()
158
+ },
6
159
  },
7
- };
160
+ })
161
+
162
+ return {
163
+ id: id,
164
+ dismiss,
165
+ update,
166
+ }
167
+ }
168
+
169
+ function useToast() {
170
+ const [state, setState] = React.useState<State>(memoryState)
171
+
172
+ React.useEffect(() => {
173
+ listeners.push(setState)
174
+ return () => {
175
+ const index = listeners.indexOf(setState)
176
+ if (index > -1) {
177
+ listeners.splice(index, 1)
178
+ }
179
+ }
180
+ }, [state])
181
+
182
+ return {
183
+ ...state,
184
+ toast,
185
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
186
+ }
8
187
  }
188
+
189
+ export { useToast, toast }
package/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  // Utilities
2
2
  export * from './lib/utils'
3
3
  export * from './lib/menu-utils'
4
+ export { BaseService } from './lib/base-service'
5
+ export { default as HttpClient, useHttpClient } from './lib/http-client'
4
6
 
5
7
  // UI Components
6
8
  export * from './components/ui'
@@ -0,0 +1,18 @@
1
+ import HttpClient from './http-client';
2
+
3
+ /**
4
+ * Classe de base pour tous les services qui utilisent HttpClient
5
+ * L'organization ID est automatiquement ajouté aux headers via les custom attributes AWS Amplify
6
+ */
7
+ export abstract class BaseService {
8
+ protected httpClient: HttpClient;
9
+
10
+ constructor(baseUrl: string) {
11
+ this.httpClient = HttpClient.getInstance(baseUrl);
12
+ }
13
+
14
+ protected handleServiceError(error: any): Error {
15
+ console.error('Service error:', error)
16
+ return error
17
+ }
18
+ }
@@ -0,0 +1,149 @@
1
+ // Simplified HTTP client without AWS Amplify dependencies
2
+
3
+ class HttpClient {
4
+ private static instance: HttpClient;
5
+ private baseUrl: string;
6
+
7
+ private constructor(baseUrl: string) {
8
+ this.baseUrl = baseUrl || process.env.NEXT_PUBLIC_API_URL || '';
9
+ }
10
+
11
+ public static getInstance(baseUrl: string): HttpClient {
12
+ if (!HttpClient.instance) {
13
+ HttpClient.instance = new HttpClient(baseUrl);
14
+ }
15
+ return HttpClient.instance;
16
+ }
17
+
18
+ private getFullUrl(path: string): string {
19
+ const cleanPath = path.startsWith('/') ? path.slice(1) : path;
20
+ return `${this.baseUrl}/${cleanPath}`;
21
+ }
22
+
23
+ private async getAuthHeaders(): Promise<Headers> {
24
+ const headers = new Headers();
25
+ headers.set('Content-Type', 'application/json');
26
+
27
+ try {
28
+ // TODO: Add authentication token and account context if needed
29
+ // For now, just return basic headers
30
+ } catch (error) {
31
+ console.warn('Failed to get auth headers:', error);
32
+ }
33
+
34
+ if (!headers.has('Content-Type')) {
35
+ headers.set('Content-Type', 'application/json');
36
+ }
37
+
38
+ return headers;
39
+ }
40
+
41
+ async request<T>(url: string, options: RequestInit = {}): Promise<T | null> {
42
+ const headers = await this.getAuthHeaders();
43
+
44
+ // Fusionner avec les headers existants
45
+ if (options.headers) {
46
+ Object.entries(options.headers).forEach(([key, value]) => {
47
+ headers.set(key, value as string);
48
+ });
49
+ }
50
+
51
+ const fullUrl = this.getFullUrl(url);
52
+
53
+ const response = await fetch(fullUrl, {
54
+ ...options,
55
+ headers,
56
+ });
57
+
58
+ if (!response.ok) {
59
+ throw new Error(`Request failed: ${response.status}`);
60
+ }
61
+
62
+ if (response.status === 204) {
63
+ return null;
64
+ }
65
+
66
+ return response.json();
67
+ }
68
+
69
+ async get<T>(url: string, options: RequestInit = {}): Promise<T | null> {
70
+ return this.request<T>(url, {
71
+ ...options,
72
+ method: 'GET'
73
+ });
74
+ }
75
+
76
+ async post<T>(url: string, body?: any, options: RequestInit = {}): Promise<T | null> {
77
+ return this.request<T>(url, {
78
+ ...options,
79
+ method: 'POST',
80
+ body: body ? JSON.stringify(body) : undefined
81
+ });
82
+ }
83
+
84
+ async put<T>(url: string, body?: any, options: RequestInit = {}): Promise<T | null> {
85
+ return this.request<T>(url, {
86
+ ...options,
87
+ method: 'PUT',
88
+ body: body ? JSON.stringify(body) : undefined
89
+ });
90
+ }
91
+
92
+ async delete<T>(url: string, options: RequestInit = {}): Promise<T | null> {
93
+ return this.request<T>(url, {
94
+ ...options,
95
+ method: 'DELETE'
96
+ });
97
+ }
98
+
99
+ async upload<T>(url: string, formData: FormData, options: RequestInit = {}): Promise<T> {
100
+ const headers = await this.getAuthHeaders();
101
+
102
+ if (headers.has('Content-Type')) {
103
+ headers.delete('Content-Type');
104
+ }
105
+
106
+ const fullUrl = this.getFullUrl(url);
107
+
108
+ const response = await fetch(fullUrl, {
109
+ ...options,
110
+ method: 'POST',
111
+ body: formData,
112
+ headers
113
+ });
114
+
115
+ if (!response.ok) {
116
+ const errorText = await response.text();
117
+ throw new Error(`Upload failed: ${response.status} ${response.statusText} - ${errorText}`);
118
+ }
119
+
120
+ if (response.status === 204) {
121
+ return null as unknown as T;
122
+ }
123
+
124
+ return response.json();
125
+ }
126
+
127
+ async download(url: string): Promise<Blob> {
128
+ const headers = await this.getAuthHeaders();
129
+ const fullUrl = this.getFullUrl(url);
130
+
131
+ const response = await fetch(fullUrl, {
132
+ method: 'GET',
133
+ headers,
134
+ credentials: 'include'
135
+ });
136
+
137
+ if (!response.ok) {
138
+ throw new Error(`Download failed: ${response.status} ${response.statusText}`);
139
+ }
140
+
141
+ return await response.blob();
142
+ }
143
+ }
144
+
145
+ export function useHttpClient(baseUrl: string) {
146
+ return HttpClient.getInstance(baseUrl);
147
+ }
148
+
149
+ export default HttpClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",