@opexa/portal-components 0.0.109 → 0.0.111
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/dist/components/KYC/CaptureIdDocument.js +196 -2
- package/dist/components/KYC/DisplayImage.d.ts +2 -0
- package/dist/components/KYC/DisplayImage.js +10 -0
- package/dist/components/KYC/IdentityVerification.js +4 -3
- package/dist/components/KYC/useKYC.d.ts +2 -0
- package/dist/components/KYC/useKYC.js +3 -0
- package/dist/components/News/News.js +1 -1
- package/dist/components/NewsFeed/NewsFeed.d.ts +4 -18
- package/dist/components/NewsFeed/NewsFeed.js +10 -10
- package/dist/components/NewsFeed/NewsFeedCarousel.d.ts +21 -0
- package/dist/components/NewsFeed/NewsFeedCarousel.js +47 -0
- package/dist/components/NewsFeed/NewsFeedList.d.ts +19 -0
- package/dist/components/NewsFeed/NewsFeedList.js +14 -0
- package/dist/icons/Tv03Icon.d.ts +2 -0
- package/dist/icons/Tv03Icon.js +4 -0
- package/dist/utils/dataUrlToBlob.d.ts +1 -0
- package/dist/utils/dataUrlToBlob.js +11 -0
- package/dist/utils/resizeImageSize.d.ts +1 -0
- package/dist/utils/resizeImageSize.js +6 -0
- package/package.json +2 -1
|
@@ -1,11 +1,205 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import { useUploadImageFileMutation } from '../../client/hooks/useUploadImageFileMutation.js';
|
|
2
6
|
import { ArrowLeftIcon } from '../../icons/ArrowLeftIcon.js';
|
|
3
7
|
import { Camera01Icon } from '../../icons/Camera01Icon.js';
|
|
8
|
+
import { CheckCircleIcon } from '../../icons/CheckCircleIcon.js';
|
|
4
9
|
import { XIcon } from '../../icons/XIcon.js';
|
|
5
10
|
import { Button } from '../../ui/Button/index.js';
|
|
6
11
|
import { Dialog } from '../../ui/Dialog/index.js';
|
|
12
|
+
import { dataURLtoBlob } from '../../utils/dataUrlToBlob.js';
|
|
13
|
+
import { resizeFile } from '../../utils/resizeImageSize.js';
|
|
7
14
|
import { useKYCContext } from './KYCContext.js';
|
|
8
15
|
export function CaptureIdDocument() {
|
|
9
16
|
const kyc = useKYCContext();
|
|
10
|
-
|
|
17
|
+
const { mutate, isPending } = useUploadImageFileMutation();
|
|
18
|
+
const [videoStream, setVideoStream] = useState(null);
|
|
19
|
+
const [cameraError, setCameraError] = useState(null);
|
|
20
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
21
|
+
const [selectedImage, setSelectedImage] = useState(null);
|
|
22
|
+
const [captureInProgress, setCaptureInProgress] = useState(false);
|
|
23
|
+
const [proccessImageCanvas, setProccessImageCanvas] = useState(null);
|
|
24
|
+
const videoElement = useRef(null);
|
|
25
|
+
const containerRef = useRef(null);
|
|
26
|
+
function stopCamera() {
|
|
27
|
+
if (videoStream) {
|
|
28
|
+
videoStream.getTracks().forEach((track) => track.stop());
|
|
29
|
+
setVideoStream(null);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
return () => {
|
|
34
|
+
stopCamera();
|
|
35
|
+
};
|
|
36
|
+
}, [videoStream]);
|
|
37
|
+
async function startCamera() {
|
|
38
|
+
try {
|
|
39
|
+
setIsLoading(true);
|
|
40
|
+
setCameraError(null);
|
|
41
|
+
setCaptureInProgress(true);
|
|
42
|
+
console.log('Starting camera...');
|
|
43
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
44
|
+
video: {
|
|
45
|
+
facingMode: 'environment',
|
|
46
|
+
aspectRatio: 16 / 9,
|
|
47
|
+
width: { ideal: 1920, max: 1920 },
|
|
48
|
+
height: { ideal: 1080, max: 1080 },
|
|
49
|
+
frameRate: {
|
|
50
|
+
ideal: 30,
|
|
51
|
+
max: 60,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
audio: false,
|
|
55
|
+
});
|
|
56
|
+
// Set the stream directly to the video element
|
|
57
|
+
if (videoElement.current) {
|
|
58
|
+
videoElement.current.srcObject = stream;
|
|
59
|
+
videoElement.current.onloadedmetadata = () => {
|
|
60
|
+
console.log('Video metadata loaded, starting playback...');
|
|
61
|
+
videoElement.current
|
|
62
|
+
?.play()
|
|
63
|
+
.then(() => {
|
|
64
|
+
setVideoStream(stream);
|
|
65
|
+
setIsLoading(false);
|
|
66
|
+
})
|
|
67
|
+
.catch(() => {
|
|
68
|
+
setCameraError('Failed to start camera stream. Please try again.');
|
|
69
|
+
setIsLoading(false);
|
|
70
|
+
// Stop the stream if playback fails
|
|
71
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
videoElement.current.onerror = (err) => {
|
|
75
|
+
console.error('Video element error:', err);
|
|
76
|
+
setCameraError('Video playback error. Please try again.');
|
|
77
|
+
setIsLoading(false);
|
|
78
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.error('Video element not available');
|
|
83
|
+
setCameraError('Camera initialization failed. Please try again.');
|
|
84
|
+
setCaptureInProgress(false);
|
|
85
|
+
setIsLoading(false);
|
|
86
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
setIsLoading(false);
|
|
91
|
+
if (error instanceof DOMException) {
|
|
92
|
+
console.error('Error accessing camera:', error.message);
|
|
93
|
+
setCameraError(error.message);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.error('Unknown error accessing camera:', error);
|
|
97
|
+
setCameraError('An unknown error occurred. Please check permissions and try again.');
|
|
98
|
+
}
|
|
99
|
+
setCaptureInProgress(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function captureAndProcessImage() {
|
|
103
|
+
if (!videoElement.current || !videoStream) {
|
|
104
|
+
console.error('Video element or stream not available for capture');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const canvas = document.createElement('canvas');
|
|
109
|
+
const parentDiv = document.getElementById('parent');
|
|
110
|
+
canvas.width = parentDiv?.clientWidth || videoElement.current.width;
|
|
111
|
+
canvas.height = parentDiv?.clientHeight || videoElement.current.height;
|
|
112
|
+
const ctx = canvas.getContext('2d');
|
|
113
|
+
if (!ctx) {
|
|
114
|
+
console.error('Could not get 2D context for canvas');
|
|
115
|
+
setCameraError('Failed to process image. Please try again.');
|
|
116
|
+
setCaptureInProgress(false);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
ctx.drawImage(videoElement.current, 0, 0, canvas.width, canvas.height);
|
|
120
|
+
const imageSrc = canvas.toDataURL('image/png', 1.0);
|
|
121
|
+
setSelectedImage({ src: imageSrc });
|
|
122
|
+
setCaptureInProgress(false);
|
|
123
|
+
stopCamera();
|
|
124
|
+
const newImg = document.createElement('img');
|
|
125
|
+
newImg.src = imageSrc;
|
|
126
|
+
//@ts-expect-error jscanify is not typed
|
|
127
|
+
const scanner = new jscanify();
|
|
128
|
+
newImg.onload = () => {
|
|
129
|
+
try {
|
|
130
|
+
if (containerRef.current) {
|
|
131
|
+
// containerRef.current.innerHTML = '';
|
|
132
|
+
const paperWidth = parentDiv?.clientWidth || videoElement.current?.videoWidth;
|
|
133
|
+
const paperHeight = parentDiv?.clientHeight || videoElement.current?.videoHeight;
|
|
134
|
+
//@ts-expect-error jscanify is not typed
|
|
135
|
+
const contour = scanner.findPaperContour(cv.imread(newImg));
|
|
136
|
+
const cornerPoints = scanner.getCornerPoints(contour);
|
|
137
|
+
const resultCanvas = scanner.extractPaper(newImg, paperWidth, paperHeight, cornerPoints);
|
|
138
|
+
newImg.className =
|
|
139
|
+
'absolute inset-0 m-auto w-full h-auto md:max-h-[333px] md:max-w-[643px] aspect-video rounded-lg object-contain';
|
|
140
|
+
setProccessImageCanvas(resultCanvas);
|
|
141
|
+
containerRef.current.appendChild(resultCanvas);
|
|
142
|
+
setCaptureInProgress(false);
|
|
143
|
+
setSelectedImage({ src: resultCanvas.toDataURL('image/png') });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
console.error('Error processing image:', error);
|
|
148
|
+
setCameraError('Failed to process image. Please try again.');
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
newImg.onerror = () => {
|
|
152
|
+
console.error('Error loading image');
|
|
153
|
+
setCameraError('Failed to load image. Please try again.');
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error('Error capturing image:', error);
|
|
158
|
+
setCameraError('Failed to capture image. Please try again.');
|
|
159
|
+
setCaptureInProgress(false);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function uploadPhoto() {
|
|
163
|
+
if (!proccessImageCanvas) {
|
|
164
|
+
console.error('Failed to process the image. Please try again.');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const blob = proccessImageCanvas.toDataURL('image/png', 1.0);
|
|
168
|
+
const processedBlob = dataURLtoBlob(blob);
|
|
169
|
+
const img = new File([processedBlob], 'frontImage', { type: 'image/jpeg' });
|
|
170
|
+
const resizedImage = (await resizeFile(img));
|
|
171
|
+
if (resizedImage) {
|
|
172
|
+
await mutate({
|
|
173
|
+
file: resizedImage,
|
|
174
|
+
}, {
|
|
175
|
+
onSuccess: (data) => {
|
|
176
|
+
kyc.setFrontImageId(data);
|
|
177
|
+
kyc.setCapturing(null);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function retakePhoto() {
|
|
183
|
+
setSelectedImage(null);
|
|
184
|
+
setCaptureInProgress(true);
|
|
185
|
+
setCameraError(null);
|
|
186
|
+
if (containerRef.current) {
|
|
187
|
+
const canvas = containerRef.current.querySelector('canvas');
|
|
188
|
+
if (canvas) {
|
|
189
|
+
canvas.width = canvas.width;
|
|
190
|
+
}
|
|
191
|
+
containerRef.current.innerHTML = '';
|
|
192
|
+
}
|
|
193
|
+
if (videoElement.current) {
|
|
194
|
+
videoElement.current.play();
|
|
195
|
+
}
|
|
196
|
+
startCamera();
|
|
197
|
+
}
|
|
198
|
+
return (_jsx(_Fragment, { children: _jsxs("div", { className: "w-full p-4 lg:w-[640px] lg:px-12 lg:py-4xl", children: [_jsxs("button", { type: "button", className: "flex items-center gap-lg lg:absolute lg:right-5 lg:top-5 lg:p-md", onClick: () => {
|
|
199
|
+
stopCamera();
|
|
200
|
+
kyc.setCapturing(null);
|
|
201
|
+
}, children: [_jsx(XIcon, { className: "text-text-senary-300 hidden size-5 lg:block" }), _jsx(ArrowLeftIcon, { className: "text-text-senary-300 size-5 lg:hidden" }), _jsx("span", { className: "text-lg font-semibold lg:hidden", children: "Back" })] }), _jsx(Dialog.Title, { className: "mt-6 text-center text-lg font-semibold lg:mt-0", children: "Take a Picture of Your Front ID" }), _jsxs(Dialog.Description, { className: "lg:max-w-auto text-text-tertiary-600 mx-auto mt-sm max-w-[19rem] text-center text-sm lg:max-w-full", children: ["Make sure your ID is clearly visible, well-lit, and not blurry.", _jsx("br", { className: "hidden lg:block" }), " Avoid glare or reflections, and ensure all corners are within the frame."] }), _jsx("div", { id: "parent", className: "border-border-disabled mt-6 aspect-video rounded-xl border bg-black lg:mt-8", children: cameraError ? (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-red-500 mb-2 text-sm", children: "Camera Error" }), _jsx("p", { className: "text-gray-400 text-xs", children: cameraError })] }) })) : captureInProgress ? (_jsxs("div", { className: "relative flex h-fit w-full flex-col items-center justify-center", children: [_jsx("video", { ref: videoElement, autoPlay: true, muted: true, playsInline: true, className: "relative h-[60vh] w-full rounded-lg object-cover md:aspect-video md:max-h-[333px]", width: "100%", height: "100%" }), _jsxs("div", { className: "pointer-events-none absolute inset-0 z-50", children: [_jsx("div", { className: twMerge('absolute inset-0 w-full bg-black/60 backdrop-blur-sm [clip-path:polygon(0%_0%,0%_100%,calc(50%-158px)_100%,calc(50%-158px)_calc(50%-100px),calc(50%+158px)_calc(50%-100px),calc(50%+158px)_calc(50%+100px),calc(50%-158px)_calc(50%+100px),calc(50%-158px)_100%,100%_100%,100%_0%)] md:[clip-path:polygon(0%_0%,0%_100%,calc(50%-225px)_100%,calc(50%-225px)_calc(50%-142px),calc(50%+225px)_calc(50%-142px),calc(50%+225px)_calc(50%+142px),calc(50%-225px)_calc(50%+142px),calc(50%-225px)_100%,100%_100%,100%_0%)]') }), _jsx("div", { className: "absolute inset-0 z-[60] mx-auto flex items-center justify-center", children: _jsx("div", { className: "relative aspect-[1011/638] h-[200px] md:h-[295px] md:w-[450px]", children: _jsx("div", { className: "h-full w-full rounded-lg border-4 border-dashed border-[#B54708]" }) }) })] })] })) : (_jsx("div", { ref: containerRef, className: "relative grid h-full w-full place-items-center rounded-lg object-contain md:max-h-[333px]" })) }), cameraError ? (_jsx("div", { className: "mt-4 text-center", children: _jsx(Button, { className: "mx-auto px-xl lg:w-fit", onClick: () => {
|
|
202
|
+
setCameraError(null);
|
|
203
|
+
startCamera();
|
|
204
|
+
}, children: "Try Again" }) })) : captureInProgress ? (_jsxs(Button, { className: "disabled:bg-bg-disabled disabled:text-text-disabled mx-auto mt-6 px-xl disabled:opacity-100 lg:mt-8 lg:w-fit", onClick: captureAndProcessImage, children: ["Capture Image", _jsx(Camera01Icon, { className: "size-5" })] })) : selectedImage && selectedImage.src ? (_jsxs("div", { className: "mt-4 flex flex-col items-center justify-center gap-2 md:flex-row", children: [_jsxs(Button, { className: "lg:w-fit", type: "button", onClick: retakePhoto, children: ["Retake Photo ", _jsx(Camera01Icon, { className: "ml-1" })] }), _jsxs(Button, { className: "lg:w-fit", type: "button", onClick: uploadPhoto, disabled: isPending, children: ["Use Photo ", _jsx(CheckCircleIcon, { className: "ml-1" })] })] })) : (_jsxs(Button, { className: "disabled:bg-bg-disabled disabled:text-text-disabled mx-auto mt-6 px-xl disabled:opacity-100 lg:mt-8 lg:w-fit", onClick: startCamera, disabled: isLoading || !!videoStream, children: [isLoading ? 'Starting Camera...' : 'Start Camera', _jsx(Camera01Icon, { className: "size-5" })] }))] }) }));
|
|
11
205
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import Image from 'next/image';
|
|
3
|
+
import { useFileQuery } from '../../client/hooks/useFileQuery.js';
|
|
4
|
+
import { useKYCContext } from './KYCContext.js';
|
|
5
|
+
function DisplayImage() {
|
|
6
|
+
const kyc = useKYCContext();
|
|
7
|
+
const { data } = useFileQuery(kyc.idFrontImageId ?? '');
|
|
8
|
+
return (_jsx("div", { className: "border-border-primary bg-bg-primary min-h-[11.25rem]", children: data?.url && (_jsx(Image, { src: data.url, alt: "Uploaded", className: "w-full object-contain", width: 500, height: 500 })) }));
|
|
9
|
+
}
|
|
10
|
+
export default DisplayImage;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useShallow } from 'zustand/shallow';
|
|
3
3
|
import { useGlobalStore } from '../../client/hooks/useGlobalStore.js';
|
|
4
4
|
import { HelpCircleIcon } from '../../icons/HelpCircleIcon.js';
|
|
@@ -7,6 +7,7 @@ import { Button } from '../../ui/Button/index.js';
|
|
|
7
7
|
import { Dialog } from '../../ui/Dialog/index.js';
|
|
8
8
|
import { Field } from '../../ui/Field/index.js';
|
|
9
9
|
import { Portal } from '../../ui/Portal/index.js';
|
|
10
|
+
import DisplayImage from './DisplayImage.js';
|
|
10
11
|
import { useKYCContext } from './KYCContext.js';
|
|
11
12
|
export function IdentityVerification() {
|
|
12
13
|
const kyc = useKYCContext();
|
|
@@ -24,9 +25,9 @@ export function IdentityVerification() {
|
|
|
24
25
|
}
|
|
25
26
|
function UploadIdDocument() {
|
|
26
27
|
const kyc = useKYCContext();
|
|
27
|
-
return (_jsxs("div", { className: "border-border-primary bg-bg-primary flex min-h-[11.25rem] flex-col justify-center gap-lg rounded-xl border px-3xl py-xl", children: [_jsx("div", { className: "border-border-primary shadow-xs mx-auto flex size-8 items-center justify-center rounded-sm border", children: _jsx(UploadCloud02Icon, { className: "text-text-quinary size-4" }) }), _jsxs("p", { className: "text-text-tertiary-600 text-center text-sm", children: [_jsx("span", { className: "text-button-secondary-fg font-semibold", children: "Click to upload" }), ' ', "or drag and drop", _jsx("br", {}), "PNG, JPG or JPEG (max. 10mb)", _jsx("br", {}), "or"] }), _jsx(Button, { size: "xs", variant: "outline", className: "mx-auto w-fit", onClick: () => {
|
|
28
|
+
return (_jsxs("div", { className: "border-border-primary bg-bg-primary flex min-h-[11.25rem] flex-col justify-center gap-lg rounded-xl border px-3xl py-xl", children: [kyc.idFrontImageId ? (_jsx(DisplayImage, {})) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "border-border-primary shadow-xs mx-auto flex size-8 items-center justify-center rounded-sm border", children: _jsx(UploadCloud02Icon, { className: "text-text-quinary size-4" }) }), _jsxs("p", { className: "text-text-tertiary-600 text-center text-sm", children: [_jsx("span", { className: "text-button-secondary-fg font-semibold", children: "Click to upload" }), ' ', "or drag and drop", _jsx("br", {}), "PNG, JPG or JPEG (max. 10mb)", _jsx("br", {}), "or"] })] })), _jsx(Button, { size: "xs", variant: "outline", className: "mx-auto w-fit", onClick: () => {
|
|
28
29
|
kyc.setCapturing('ID_DOCUMENT');
|
|
29
|
-
}, children:
|
|
30
|
+
}, children: kyc.idFrontImageId ? 'Rekate Photo' : 'Take a Photo' })] }));
|
|
30
31
|
}
|
|
31
32
|
function UploadSelfie() {
|
|
32
33
|
const kyc = useKYCContext();
|
|
@@ -17,6 +17,8 @@ export interface UseKYCReturn {
|
|
|
17
17
|
setCapturing: (capturing: CaptureSubject | null) => void;
|
|
18
18
|
subscribe: (subscriber: Subscriber) => UnsubscribeFn;
|
|
19
19
|
triggerEvent: (detail: EventDetail) => void;
|
|
20
|
+
idFrontImageId: string | null;
|
|
21
|
+
setFrontImageId: (id: string | null) => void;
|
|
20
22
|
}
|
|
21
23
|
export declare function useKYC(): UseKYCReturn;
|
|
22
24
|
export {};
|
|
@@ -2,6 +2,7 @@ import { useRef, useState } from 'react';
|
|
|
2
2
|
export function useKYC() {
|
|
3
3
|
const [step, setStep] = useState(1);
|
|
4
4
|
const [done, setDone] = useState(false);
|
|
5
|
+
const [idFrontImageId, setFrontImageId] = useState(null);
|
|
5
6
|
const [capturing, setCapturing] = useState(null);
|
|
6
7
|
const subscribers = useRef([]);
|
|
7
8
|
const subscribe = (subscriber) => {
|
|
@@ -22,6 +23,8 @@ export function useKYC() {
|
|
|
22
23
|
setDone,
|
|
23
24
|
capturing,
|
|
24
25
|
setCapturing,
|
|
26
|
+
idFrontImageId,
|
|
27
|
+
setFrontImageId,
|
|
25
28
|
subscribe,
|
|
26
29
|
triggerEvent,
|
|
27
30
|
};
|
|
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
|
|
4
4
|
import { ChevronLeftIcon } from '../../icons/ChevronLeftIcon.js';
|
|
5
5
|
import { Prose } from '../../ui/Prose/index.js';
|
|
6
6
|
export function News(props) {
|
|
7
|
-
return (_jsxs("div", { style: props.style, className: props.className, children: [_jsxs(Link, { href: props.viewAllUrl ?? '/news', className: "text-button-secondary-fg flex items-center gap-lg text-lg font-semibold", children: [_jsx(ChevronLeftIcon, { className: "size-6" }), "Back to all News"] }), _jsx(Image, { src: props.news.image, alt: "", width:
|
|
7
|
+
return (_jsxs("div", { style: props.style, className: props.className, children: [_jsxs(Link, { href: props.viewAllUrl ?? '/news', className: "text-button-secondary-fg flex items-center gap-lg text-lg font-semibold", children: [_jsx(ChevronLeftIcon, { className: "size-6" }), "Back to all News"] }), _jsx(Image, { src: props.news.image, alt: "", width: 1024, height: 600, className: "mt-3xl block h-auto w-full rounded-xl lg:mt-6" }), _jsx("p", { className: "text-brand-400 mt-xl text-sm font-semibold lg:mt-6", children: props.news.category }), _jsx("h2", { className: "mt-xl text-2xl font-semibold lg:mt-6", children: props.news.title }), _jsx(Prose, { className: "mt-xl lg:mt-6", dangerouslySetInnerHTML: {
|
|
8
8
|
__html: props.news.content,
|
|
9
9
|
} })] }));
|
|
10
10
|
}
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
/** @default 'News' */
|
|
6
|
-
heading?: string;
|
|
7
|
-
/**
|
|
8
|
-
* @default '/news'
|
|
9
|
-
* @example
|
|
10
|
-
* ```ts
|
|
11
|
-
* '/custom-path' // '/custom-path/123'
|
|
12
|
-
* ```
|
|
13
|
-
*/
|
|
14
|
-
viewDetailsUrl?: string;
|
|
15
|
-
className?: string;
|
|
16
|
-
style?: CSSProperties;
|
|
17
|
-
}
|
|
18
|
-
export declare function NewsFeed(props: NewsFeedProps): import("react/jsx-runtime").JSX.Element;
|
|
1
|
+
import { type NewsFeedCarouselProps } from './NewsFeedCarousel';
|
|
2
|
+
import { type NewsFeedListProps } from './NewsFeedList';
|
|
3
|
+
export type NewsFeedProps = NewsFeedListProps | NewsFeedCarouselProps;
|
|
4
|
+
export declare function NewsFeed(props: NewsFeedProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { NewsFeedCarousel, } from './NewsFeedCarousel.js';
|
|
3
|
+
import { NewsFeedList } from './NewsFeedList.js';
|
|
4
|
+
export async function NewsFeed(props) {
|
|
5
|
+
if (props.layout === 'list') {
|
|
6
|
+
return _jsx(NewsFeedList, { ...props });
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
return _jsx(NewsFeedCarousel, { ...props });
|
|
10
|
+
}
|
|
11
11
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
import type { News } from '../../types';
|
|
3
|
+
export interface NewsFeedCarouselProps {
|
|
4
|
+
layout: 'carousel';
|
|
5
|
+
newsFeed: News[];
|
|
6
|
+
/** @default 'News' */
|
|
7
|
+
heading?: string;
|
|
8
|
+
/** @default '/news' */
|
|
9
|
+
viewAllUrl?: string;
|
|
10
|
+
/**
|
|
11
|
+
* @default '/news'
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* '/custom-path' // '/custom-path/123'
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
viewDetailsUrl?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
export declare function NewsFeedCarousel(props: NewsFeedCarouselProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import useEmblaCarousel, {} from 'embla-carousel-react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
7
|
+
import { twMerge } from 'tailwind-merge';
|
|
8
|
+
import { ArrowLeftIcon } from '../../icons/ArrowLeftIcon.js';
|
|
9
|
+
import { ArrowRightIcon } from '../../icons/ArrowRightIcon.js';
|
|
10
|
+
import { ChevronRightIcon } from '../../icons/ChevronRightIcon.js';
|
|
11
|
+
import { Tv03Icon } from '../../icons/Tv03Icon.js';
|
|
12
|
+
import { Button } from '../../ui/Button/index.js';
|
|
13
|
+
import { Empty } from '../shared/Empty.js';
|
|
14
|
+
export function NewsFeedCarousel(props) {
|
|
15
|
+
const empty = props.newsFeed.length <= 0;
|
|
16
|
+
const heading = props.heading ?? 'News';
|
|
17
|
+
const viewAllUrl = props.viewAllUrl ?? '/news';
|
|
18
|
+
const viewDetailsUrl = props.viewDetailsUrl ?? '/news';
|
|
19
|
+
const [emblaRef, emblaApi] = useEmblaCarousel({
|
|
20
|
+
align: 'start',
|
|
21
|
+
slidesToScroll: 1,
|
|
22
|
+
breakpoints: {
|
|
23
|
+
'(min-width: 1024px)': {
|
|
24
|
+
slidesToScroll: 3,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true);
|
|
29
|
+
const [nextBtnDisabled, setNextBtnDisabled] = useState(true);
|
|
30
|
+
const onPrevButtonClick = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
|
|
31
|
+
const onNextButtonClick = useCallback(async () => {
|
|
32
|
+
emblaApi?.scrollNext();
|
|
33
|
+
}, [emblaApi]);
|
|
34
|
+
const onSelect = useCallback((emblaApi) => {
|
|
35
|
+
setPrevBtnDisabled(!emblaApi?.canScrollPrev());
|
|
36
|
+
setNextBtnDisabled(!emblaApi?.canScrollNext());
|
|
37
|
+
}, []);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!emblaApi)
|
|
40
|
+
return;
|
|
41
|
+
onSelect(emblaApi);
|
|
42
|
+
emblaApi.on('reInit', onSelect).on('select', onSelect);
|
|
43
|
+
}, [emblaApi, onSelect]);
|
|
44
|
+
return (_jsxs("div", { style: props.style, className: props.className, children: [_jsxs("div", { className: "flex items-center", children: [_jsx("h2", { className: "text-lg font-semibold", children: heading }), _jsx("div", { className: "grow" }), _jsxs("div", { className: "flex items-center justify-center gap-xl", children: [_jsxs(Link, { href: viewAllUrl, className: "text-button-tertiary-fg flex gap-sm text-sm font-semibold", children: ["See All", _jsx(ChevronRightIcon, { className: "size-5 lg:hidden" })] }), _jsxs("div", { className: "hidden lg:flex", children: [_jsx(Button, { variant: "outline", colorScheme: "gray", className: "rounded-r-none border-r-0", onClick: onPrevButtonClick, disabled: prevBtnDisabled, "aria-label": "Previous", children: _jsx(ArrowLeftIcon, { className: "size-5" }) }), _jsx(Button, { variant: "outline", colorScheme: "gray", className: "rounded-l-none", onClick: onNextButtonClick, disabled: nextBtnDisabled, "aria-label": "Next", children: _jsx(ArrowRightIcon, { className: "size-5" }) })] })] })] }), empty && (_jsx(Empty, { icon: Tv03Icon, title: "No News", message: "No news is currently available.", className: "mt-8" })), !empty && (_jsx(_Fragment, { children: _jsx("div", { ref: emblaRef, className: "relative mt-lg lg:overflow-hidden", children: _jsx("div", { className: twMerge('grid auto-cols-[100%] grid-flow-col gap-1.5 lg:auto-cols-[calc((100%-(1.25rem*2))/3)] lg:gap-2xl'), children: props.newsFeed.map((news) => (_jsxs("div", { className: "bg-bg-primary-alt border-border-primary flex flex-col overflow-hidden rounded-2xl border shadow-md", children: [_jsx(Image, { src: news.image, alt: "", width: 356, height: 244, className: "block h-auto w-full" }), _jsxs("div", { className: "flex grow flex-col p-3xl", children: [_jsxs("div", { className: "grow", children: [_jsx("p", { className: "text-brand-400 text-sm font-semibold", children: news.category }), _jsx("h2", { className: "mt-2 text-lg font-semibold", children: news.title }), _jsx("p", { className: "text-text-tertiary-600 mt-xs line-clamp-3", dangerouslySetInnerHTML: {
|
|
45
|
+
__html: news.content,
|
|
46
|
+
} })] }), _jsx(Button, { asChild: true, className: "mt-2xl", children: _jsx(Link, { href: `${viewDetailsUrl}/${news.id}`, children: "Read More" }) })] })] }, news.id))) }) }) }))] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import type { News } from '../../types';
|
|
3
|
+
export interface NewsFeedListProps {
|
|
4
|
+
layout: 'list';
|
|
5
|
+
newsFeed: News[];
|
|
6
|
+
/** @default 'News' */
|
|
7
|
+
heading?: string;
|
|
8
|
+
/**
|
|
9
|
+
* @default '/news'
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* '/custom-path' // '/custom-path/123'
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
viewDetailsUrl?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
style?: CSSProperties;
|
|
18
|
+
}
|
|
19
|
+
export declare function NewsFeedList(props: NewsFeedListProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Image from 'next/image';
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { Tv03Icon } from '../../icons/Tv03Icon.js';
|
|
5
|
+
import { Button } from '../../ui/Button/index.js';
|
|
6
|
+
import { Empty } from '../shared/Empty.js';
|
|
7
|
+
export function NewsFeedList(props) {
|
|
8
|
+
const empty = props.newsFeed.length <= 0;
|
|
9
|
+
const heading = props.heading ?? 'News';
|
|
10
|
+
const viewDetailsUrl = props.viewDetailsUrl ?? '/news';
|
|
11
|
+
return (_jsxs("div", { style: props.style, className: props.className, children: [_jsx("h2", { className: "text-lg font-semibold", children: heading }), empty && (_jsx(Empty, { icon: Tv03Icon, title: "No News", message: "No news is currently available.", className: "mt-8" })), !empty && (_jsx("div", { className: "mt-lg grid gap-3xl lg:grid-cols-3 lg:gap-2xl", children: props.newsFeed.map((news) => (_jsxs("div", { className: "bg-bg-primary-alt border-border-primary flex flex-col overflow-hidden rounded-2xl border shadow-md", children: [_jsx(Image, { src: news.image, alt: "", width: 356, height: 244, className: "block h-auto w-full" }), _jsxs("div", { className: "flex grow flex-col p-3xl", children: [_jsxs("div", { className: "grow", children: [_jsx("p", { className: "text-brand-400 text-sm font-semibold", children: news.category }), _jsx("h2", { className: "mt-2 text-lg font-semibold", children: news.title }), _jsx("p", { className: "text-text-tertiary-600 mt-xs line-clamp-3", dangerouslySetInnerHTML: {
|
|
12
|
+
__html: news.content,
|
|
13
|
+
} })] }), _jsx(Button, { asChild: true, className: "mt-2xl", children: _jsx(Link, { href: `${viewDetailsUrl}/${news.id}`, children: "Read More" }) })] })] }, news.id))) }))] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function Tv03Icon(props) {
|
|
3
|
+
return (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: _jsx("path", { d: "M17 3L12 7L7 3M6.8 21H17.2C18.8802 21 19.7202 21 20.362 20.673C20.9265 20.3854 21.3854 19.9265 21.673 19.362C22 18.7202 22 17.8802 22 16.2V11.8C22 10.1198 22 9.27976 21.673 8.63803C21.3854 8.07354 20.9265 7.6146 20.362 7.32698C19.7202 7 18.8802 7 17.2 7H6.8C5.11984 7 4.27976 7 3.63803 7.32698C3.07354 7.6146 2.6146 8.07354 2.32698 8.63803C2 9.27976 2 10.1198 2 11.8V16.2C2 17.8802 2 18.7202 2.32698 19.362C2.6146 19.9265 3.07354 20.3854 3.63803 20.673C4.27976 21 5.11984 21 6.8 21Z" }) }));
|
|
4
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function dataURLtoBlob(dataURL: string): Blob;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function dataURLtoBlob(dataURL) {
|
|
2
|
+
const parts = dataURL.split(';base64,');
|
|
3
|
+
const contentType = parts[0].split(':')[1];
|
|
4
|
+
const raw = window.atob(parts[1]);
|
|
5
|
+
const rawLength = raw.length;
|
|
6
|
+
const uInt8Array = new Uint8Array(rawLength);
|
|
7
|
+
for (let i = 0; i < rawLength; ++i) {
|
|
8
|
+
uInt8Array[i] = raw.charCodeAt(i);
|
|
9
|
+
}
|
|
10
|
+
return new Blob([uInt8Array], { type: contentType });
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const resizeFile: (file: File) => Promise<unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opexa/portal-components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.111",
|
|
4
4
|
"exports": {
|
|
5
5
|
"./ui/*": {
|
|
6
6
|
"types": "./dist/ui/*/index.d.ts",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"embla-carousel-react": "^8.6.0",
|
|
87
87
|
"is-mobile": "^5.0.0",
|
|
88
88
|
"lodash-es": "^4.17.21",
|
|
89
|
+
"react-image-file-resizer": "^0.4.8",
|
|
89
90
|
"tailwind-merge": "^3.3.1",
|
|
90
91
|
"tailwind-variants": "^1.0.0",
|
|
91
92
|
"tailwind-zag": "^1.0.2",
|