@@ -126,8 +126,8 @@ export default function SelectHtmlCaptureModal(props: SelectHtmlCaptureModalProp
))}
) : (
-
-
Install the extension to capture HTML
+
+
Install and use the extension to capture screens.
)}
@@ -143,7 +143,7 @@ export default function SelectHtmlCaptureModal(props: SelectHtmlCaptureModalProp
}}
disabled={!selectedHtmlCaptureId}
>
- Select HTML Capture
+ Select Capture
)}
diff --git a/academy-ui/src/components/clickableDemos/Edit/EditClickableDemoStepperItem.tsx b/academy-ui/src/components/clickableDemos/Edit/EditClickableDemoStepperItem.tsx
index a111382d2..dcfcb3794 100644
--- a/academy-ui/src/components/clickableDemos/Edit/EditClickableDemoStepperItem.tsx
+++ b/academy-ui/src/components/clickableDemos/Edit/EditClickableDemoStepperItem.tsx
@@ -1,5 +1,3 @@
-import CaptureInput from '@/components/clickableDemos/CaptureSelector/CaptureInput';
-import SelectElementInput from '@/components/clickableDemos/ElementSelector/SelectElementInput';
import { ClickableDemoStepInput, UpsertClickableDemoInput } from '@/graphql/generated/generated-types';
import { TooltipPlacement } from '@/types/clickableDemos/ClickableDemoDto';
import { SpaceWithIntegrationsDto } from '@/types/space/SpaceDto';
@@ -11,6 +9,10 @@ import { ClickableDemoErrors, ClickableDemoStepError } from '@dodao/web-core/typ
import { slugify } from '@dodao/web-core/utils/auth/slugify';
import { useState } from 'react';
import styles from './EditClickableDemoStepperItem.module.scss';
+import EditableImage from '@dodao/web-core/components/core/image/EditableImage';
+import { ClickableDemoHtmlCaptureDto } from '@/types/html-captures/ClickableDemoHtmlCaptureDto';
+import SelectHtmlCaptureModal from '@/components/clickableDemoHtmlCapture/SelectHtmlCaptureModal';
+import ElementSelectorModal from '../ElementSelector/ElementSelectorModal';
interface StepProps {
space: SpaceWithIntegrationsDto;
@@ -58,6 +60,16 @@ export default function EditClickableDemoStepperItem({
onUpdateStep,
uploadToS3AndReturnScreenshotUrl,
}: StepProps) {
+ const [showSelectHtmlCaptureModal, setShowSelectHtmlCaptureModal] = useState(false);
+ const [showElementSelectorModal, setShowElementSelectorModal] = useState(false);
+
+ const inputId = 'capture-input';
+
+ const handleSelectHtmlCapture = (selectedCapture: ClickableDemoHtmlCaptureDto) => {
+ updateStepUrl(selectedCapture.fileUrl, selectedCapture.fileImageUrl);
+ setShowSelectHtmlCaptureModal(false);
+ };
+
const [uploadHTMLFileLoading, setUploadHTMLFileLoading] = useState(false);
const updateStepSelector = (selector: string | number | undefined, elementImgUrl: string | undefined) => {
@@ -82,8 +94,6 @@ export default function EditClickableDemoStepperItem({
return error ? error.toString() : '';
};
- console.log('step.placement', step.placement);
-
return (
@@ -118,43 +128,53 @@ export default function EditClickableDemoStepperItem({
/>
-
-
-
- {step.screenImgUrl && (
-
-
-
- )}
-
-
-
-
-
- {step.elementImgUrl && ( // Assuming `step.screenImgUrl` holds the URL of the screenshot image
-
-
-
- )}
-
+
+
+ updateStepUrl('', '')}
+ onUpload={() => setShowSelectHtmlCaptureModal(true)}
+ height="200px"
+ error={inputError('url') ? 'Screen Capture is required' : ''}
+ />
+
+ updateStepSelector('', '')}
+ onUpload={() => setShowElementSelectorModal(true)}
+ height="150px"
+ maxWidth="250px"
+ disabled={step.screenImgUrl != ''}
+ disabledTooltip="Please select a capture first"
+ error={inputError('selector') ? 'Selector is required' : ''}
+ />
+ {showSelectHtmlCaptureModal && (
+
setShowSelectHtmlCaptureModal(false)}
+ selectHtmlCapture={handleSelectHtmlCapture}
+ demoId={clickableDemo.id}
+ spaceId={space.id}
+ />
+ )}
+ {showElementSelectorModal && (
+
+ )}
);
}
diff --git a/academy-ui/src/components/shortVideos/Edit/EditShortVideoForm.tsx b/academy-ui/src/components/shortVideos/Edit/EditShortVideoForm.tsx
index 8d1d81180..d35a9a6f2 100644
--- a/academy-ui/src/components/shortVideos/Edit/EditShortVideoForm.tsx
+++ b/academy-ui/src/components/shortVideos/Edit/EditShortVideoForm.tsx
@@ -1,6 +1,4 @@
-import MarkdownEditor from '@/components/app/Markdown/MarkdownEditor';
-import UploadInput from '@/components/app/UploadInput';
-import { ImageType, ShortVideo, ShortVideoInput } from '@/graphql/generated/generated-types';
+import { ImageType, ShortVideo, ShortVideoInput, CreateSignedUrlInput } from '@/graphql/generated/generated-types';
import Button from '@dodao/web-core/components/core/buttons/Button';
import Input from '@dodao/web-core/components/core/input/Input';
import { useNotificationContext } from '@dodao/web-core/ui/contexts/NotificationContext';
@@ -14,6 +12,14 @@ import getBaseUrl from '@dodao/web-core/utils/api/getBaseURL';
import PageWrapper from '@dodao/web-core/components/core/page/PageWrapper';
import SingleCardLayout from '@/layouts/SingleCardLayout';
import { useI18 } from '@/hooks/useI18';
+import EditableImage from '@dodao/web-core/components/core/image/EditableImage';
+import EditableVideo from '@dodao/web-core/components/core/image/EditableVideo';
+import UploadImageFromDeviceModal from '@/components/app/Image/UploadImageFromDeviceModal';
+import axios from 'axios';
+import { usePostData } from '@dodao/web-core/ui/hooks/fetch/usePostData';
+import { CreateSignedUrlRequest } from '@/types/request/SignedUrl';
+import { SingedUrlResponse } from '@/types/response/SignedUrl';
+import { getUploadedImageUrlFromSingedUrl } from '@dodao/web-core/utils/upload/getUploadedImageUrlFromSingedUrl';
export interface EditShortVideoModalProps {
shortVideoToEdit?: ShortVideo;
@@ -54,6 +60,9 @@ export default function EditShortVideoModal({
const router = useRouter();
const [shortVideoUpserting, setShortVideoUpserting] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [selectImageUploadModal, setSelectImageUploadModal] = useState(false);
+ const [uploadFileLoading, setUploadFileLoading] = useState(false);
+
const threeDotItems: EllipsisDropdownItem[] = [{ label: 'Delete', key: 'delete' }];
const { $t } = useI18();
@@ -75,6 +84,12 @@ export default function EditShortVideoModal({
}
const { showNotification } = useNotificationContext();
+ const { postData } = usePostData
(
+ {
+ errorMessage: 'Failed to get signed URL',
+ },
+ {}
+ );
const upsertShortVideo = async () => {
const errors: Record = {
id: null,
@@ -123,75 +138,99 @@ export default function EditShortVideoModal({
}
};
+ async function uploadToS3AndReturnImgUrl(file: File) {
+ setUploadFileLoading(true);
+ const input: CreateSignedUrlInput = {
+ imageType: ImageType.ShortVideo,
+ contentType: file.type,
+ objectId: (shortVideo.id || 'new-short-video' + '-short-video').replace(/[^a-z0-9]/gi, '_'),
+ name: file.name.replace(' ', '_').toLowerCase(),
+ };
+
+ const response = await postData(`${getBaseUrl()}/api/s3-signed-urls`, { spaceId, input });
+ const signedUrl = response?.url!;
+ await axios.put(signedUrl, file, {
+ headers: { 'Content-Type': file.type },
+ });
+
+ const imageUrl = getUploadedImageUrlFromSingedUrl(signedUrl);
+ updateShortVideoField('videoUrl', imageUrl?.toString() || '');
+
+ setUploadFileLoading(false);
+ }
+
return (
-
-
-
- {shortVideoToEdit && (
-
{
- if (key === 'delete') {
- setShowDeleteModal(true);
- }
- }}
- className="ml-4"
- />
- )}
+
+
+
+
+ {shortVideoToEdit && (
+
{
+ if (key === 'delete') {
+ setShowDeleteModal(true);
+ }
+ }}
+ className="ml-4"
+ />
+ )}
+
+
+
+ updateShortVideoField('title', v?.toString() || '')}
+ label="Title*"
+ required
+ placeholder="only 32 characters"
+ error={shortVideoErrors['title']}
+ />
+
+
+ updateShortVideoField('description', v?.toString() || '')}
+ error={shortVideoErrors['description']}
+ />
+
+
+
+ updateShortVideoField('thumbnail', '')}
+ onUpload={() => setSelectImageUploadModal(true)}
+ height="200px"
+ label="Select Thumbnail"
+ afterUploadLabel="Thumbnail Selected"
+ error={shortVideoErrors['thumbnail']}
+ />
+
+ updateShortVideoField('videoUrl', '')}
+ height="200px"
+ label="Select Video"
+ afterUploadLabel="Video Selected"
+ onUpload={uploadToS3AndReturnImgUrl}
+ loading={uploadFileLoading}
+ error={shortVideoErrors['videoUrl']}
+ />
-
-
- updateShortVideoField('title', v?.toString() || '')}
- label="Title*"
- required
- placeholder="only 32 characters"
- error={shortVideoErrors['title']}
- />
-
-
- updateShortVideoField('description', v?.toString() || '')}
- error={shortVideoErrors['description']}
- />
-
- updateShortVideoField('thumbnail', value?.toString() || '')}
- label={'Thumbnail*'}
- placeholder="e.g. https://example.com/thumbnail.png"
- />
- updateShortVideoField('videoUrl', value?.toString() || '')}
- allowedFileTypes={['video/mp4', 'video/x-m4v', 'video/*']}
- label={'Video*'}
- placeholder="e.g. https://example.com/video.mp4"
- />
-
-
-
upsertShortVideo()} loading={shortVideoUpserting} variant="contained" primary>
- Save
-
+
+ upsertShortVideo()} loading={shortVideoUpserting} variant="contained" primary>
+ Save
+
+
-
+
{showDeleteModal && (
)}
+ {selectImageUploadModal && (
+
setSelectImageUploadModal(false)}
+ imageType={ImageType.ShortVideo}
+ objectId={shortVideo.id || 'new-short-video' + '-short-video'}
+ spaceId={spaceId}
+ imageUploaded={(imageUrl) => {
+ updateShortVideoField('thumbnail', imageUrl?.toString() || '');
+ setSelectImageUploadModal(false);
+ }}
+ modelValue={shortVideo.thumbnail || undefined}
+ />
+ )}
);
}
diff --git a/shared/web-core/next-env.d.ts b/shared/web-core/next-env.d.ts
new file mode 100644
index 000000000..40c3d6809
--- /dev/null
+++ b/shared/web-core/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/shared/web-core/src/components/core/file/ImageUploadSection.tsx b/shared/web-core/src/components/core/file/ImageUploadSection.tsx
index 927dddbfa..260318a31 100644
--- a/shared/web-core/src/components/core/file/ImageUploadSection.tsx
+++ b/shared/web-core/src/components/core/file/ImageUploadSection.tsx
@@ -37,7 +37,6 @@ export default function ImageUploadSection({
}: ImageUploadSectionProps) {
const inputId = spaceId + '-' + slugify(label || imageType || objectId);
const [showFullScreenModal, setShowFullScreenModal] = useState(false);
- const [imageLoaded, setImageLoaded] = useState(false);
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
@@ -103,31 +102,27 @@ export default function ImageUploadSection({
src={modelValue}
alt="Uploaded file"
title="Click image to view full screen"
- className="w-full h-full object-contain cursor-pointer"
+ className="w-full h-full object-contain cursor-zoom-in"
onClick={() => setShowFullScreenModal(true)}
- onLoad={() => setImageLoaded(true)}
/>
- {imageLoaded && (
-
-
-
-
-
-
- {
- clearSelectedImage();
- setImageLoaded(false);
- }}
- />
-
+
+
+
+
+
+
+ {
+ clearSelectedImage();
+ }}
+ />
- )}
+
) : (
diff --git a/academy-ui/src/components/bytes/Edit/EditByteStepperItem.module.scss b/shared/web-core/src/components/core/image/EditableImage.module.scss
similarity index 100%
rename from academy-ui/src/components/bytes/Edit/EditByteStepperItem.module.scss
rename to shared/web-core/src/components/core/image/EditableImage.module.scss
diff --git a/shared/web-core/src/components/core/image/EditableImage.tsx b/shared/web-core/src/components/core/image/EditableImage.tsx
new file mode 100644
index 000000000..8dff4198c
--- /dev/null
+++ b/shared/web-core/src/components/core/image/EditableImage.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import dummyImage from '@dodao/web-core/images/image-placeholder.png';
+import IconButton from '@dodao/web-core/components/core/buttons/IconButton';
+import OverlayOnHover from '@dodao/web-core/components/core/overlay/OverlayOnHover';
+import ViewEditableImage from '@dodao/web-core/components/core/image/ViewEditableImage';
+import { IconTypes } from '@dodao/web-core/components/core/icons/IconTypes';
+import styles from './EditableImage.module.scss';
+
+interface EditableImageProps {
+ imageUrl?: string | null;
+ onRemove: () => void;
+ onUpload: () => void;
+ height?: string;
+ maxWidth?: string;
+ label?: string;
+ afterUploadLabel?: string;
+ disabled?: boolean;
+ disabledTooltip?: string;
+ error?: string | boolean;
+}
+
+export default function EditableImage({
+ imageUrl,
+ onRemove,
+ onUpload,
+ height = '150px',
+ maxWidth = '100%',
+ label,
+ afterUploadLabel,
+ disabled,
+ disabledTooltip = '',
+ error,
+}: EditableImageProps) {
+ return (
+
+ {imageUrl ? (
+
+
+
+
+
+
+
+
+ {afterUploadLabel && !error &&
{afterUploadLabel}
}
+
+ ) : (
+
+
+
+
+
+
+
+ {label && !error &&
{label}
}
+ {typeof error === 'string' &&
{error}
}
+
+ )}
+
+ );
+}
diff --git a/shared/web-core/src/components/core/image/EditableVideo.module.scss b/shared/web-core/src/components/core/image/EditableVideo.module.scss
new file mode 100644
index 000000000..ba35bebfe
--- /dev/null
+++ b/shared/web-core/src/components/core/image/EditableVideo.module.scss
@@ -0,0 +1,9 @@
+.iconsColorToggle {
+ background-color: var(--text-color);
+ color: var(--primary-color);
+
+ &:hover {
+ background-color: var(--primary-color);
+ color: var(--text-color);
+ }
+}
\ No newline at end of file
diff --git a/shared/web-core/src/components/core/image/EditableVideo.tsx b/shared/web-core/src/components/core/image/EditableVideo.tsx
new file mode 100644
index 000000000..937a321e8
--- /dev/null
+++ b/shared/web-core/src/components/core/image/EditableVideo.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import dummyVideo from '@dodao/web-core/images/video-placeholder.png';
+import IconButton from '@dodao/web-core/components/core/buttons/IconButton';
+import OverlayOnHover from '@dodao/web-core/components/core/overlay/OverlayOnHover';
+import { IconTypes } from '@dodao/web-core/components/core/icons/IconTypes';
+import styles from './EditableVideo.module.scss';
+import WebCoreFileUploader from '@dodao/web-core/components/core/uploadInput/FileUploader';
+
+interface EditableVideoProps {
+ videoUrl?: string | null;
+ onRemove: () => void;
+ onUpload: (file: File) => Promise
;
+ height?: string;
+ label?: string;
+ afterUploadLabel?: string;
+ disabled?: boolean;
+ disabledTooltip?: string;
+ loading: boolean;
+ error?: string | boolean;
+}
+
+export default function EditableVideo({
+ videoUrl,
+ onRemove,
+ onUpload,
+ height = '150px',
+ label,
+ afterUploadLabel,
+ disabled,
+ disabledTooltip = '',
+ loading,
+ error,
+}: EditableVideoProps) {
+ return (
+
+ {videoUrl ? (
+
+
+
+
+
+
+ {
+ onRemove();
+ onUpload(file);
+ }}
+ loading={loading}
+ >
+
+
+
+
+
+
+ {afterUploadLabel && !error &&
{afterUploadLabel}
}
+
+ ) : (
+
+
+
+
+
+
+
+
+
+ {label && !error &&
{label}
}
+ {typeof error === 'string' &&
{error}
}
+
+ )}
+
+ );
+}
diff --git a/shared/web-core/src/components/core/select/StyledSelect.tsx b/shared/web-core/src/components/core/select/StyledSelect.tsx
index 39818a124..1f5f1b848 100644
--- a/shared/web-core/src/components/core/select/StyledSelect.tsx
+++ b/shared/web-core/src/components/core/select/StyledSelect.tsx
@@ -28,6 +28,7 @@ const StyledListboxButton = styled(Listbox.Button)`
const StyledListboxOptions = styled(Listbox.Options)`
background-color: var(--bg-color);
+ border: 1px solid var(--border-color);
.active {
background-color: var(--primary-color);
}
diff --git a/shared/web-core/src/components/core/uploadInput/FileUploader.tsx b/shared/web-core/src/components/core/uploadInput/FileUploader.tsx
index 7be0760df..8e55561d9 100644
--- a/shared/web-core/src/components/core/uploadInput/FileUploader.tsx
+++ b/shared/web-core/src/components/core/uploadInput/FileUploader.tsx
@@ -34,6 +34,10 @@ export default function FileUploader({ loading, children, className, allowedFile
await uploadFile(file);
};
+ const handleClick = () => {
+ inputRef.current?.click();
+ };
+
return (
{loading ? (
@@ -42,8 +46,10 @@ export default function FileUploader({ loading, children, className, allowedFile
) : (
-
- {children}
+
+
+ {children}
+
)}
diff --git a/academy-ui/src/images/TidbitsHub/image-placeholder.png b/shared/web-core/src/images/image-placeholder.png
similarity index 100%
rename from academy-ui/src/images/TidbitsHub/image-placeholder.png
rename to shared/web-core/src/images/image-placeholder.png
diff --git a/shared/web-core/src/images/video-placeholder.png b/shared/web-core/src/images/video-placeholder.png
new file mode 100644
index 000000000..b3ad12abe
Binary files /dev/null and b/shared/web-core/src/images/video-placeholder.png differ
diff --git a/tasks/all-tasks.md b/tasks/all-tasks.md
index f735eda56..06a295b44 100644
--- a/tasks/all-tasks.md
+++ b/tasks/all-tasks.md
@@ -38,6 +38,7 @@ Must have features
- [ ] Making it easy for the admin to login. Right now we are not showing any login button. May be we show in the footer? or somewhere else where its not too visible?
### Tidbits
+- [ ] Edit modal - in stepper - image uploader - if gif is uploaded, edit and trash icon doesnt appear
- [ ] Edit modal - in stepper - icon to duplicate a step
- [ ] Edit modal - in stepper - bit of padding left & right for the closed accordion content (name+arrow)
- [ ] Edit modal - in stepper - add border to image display mode dropdown