Skip to content

Commit 4e4fae0

Browse files
authored
feat: add FeedbackImage component and integrate it into ImagePreviewButton and SheetDetailTable (#2011)
1 parent 36a51a1 commit 4e4fae0

File tree

4 files changed

+106
-82
lines changed

4 files changed

+106
-82
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* Copyright 2025 LY Corporation
3+
*
4+
* LY Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
import { useMemo } from 'react';
17+
import Image from 'next/image';
18+
import { useRouter } from 'next/router';
19+
20+
import { useOAIQuery } from '../lib';
21+
22+
interface Props {
23+
url: string;
24+
}
25+
26+
const FeedbackImage = ({ url }: Props) => {
27+
const router = useRouter();
28+
const projectId = Number(router.query.projectId);
29+
const channelId = Number(router.query.channelId);
30+
31+
const { data: channelData } = useOAIQuery({
32+
path: '/api/admin/projects/{projectId}/channels/{channelId}',
33+
variables: { channelId, projectId },
34+
queryOptions: {
35+
enabled:
36+
router.isReady &&
37+
Number.isFinite(projectId) &&
38+
Number.isFinite(channelId),
39+
},
40+
});
41+
42+
const imageKey = useMemo(() => {
43+
if (!channelData?.imageConfig?.enablePresignedUrlDownload) return url;
44+
45+
let parsed: URL | null = null;
46+
try {
47+
parsed = new URL(
48+
url,
49+
typeof window !== 'undefined' ?
50+
window.location.href
51+
: 'http://localhost',
52+
);
53+
} catch {
54+
return url;
55+
}
56+
if (!/^https?:$/.test(parsed.protocol)) return url;
57+
58+
const rawPath = parsed.pathname.replace(/^\/+/, '');
59+
let key = rawPath;
60+
try {
61+
key = decodeURIComponent(rawPath);
62+
} catch {
63+
/* keep raw */
64+
}
65+
66+
const host = parsed.hostname;
67+
const parts = key.split('/', 2);
68+
if (
69+
(host === 's3.amazonaws.com' || host.startsWith('s3.')) &&
70+
parts.length >= 2
71+
) {
72+
return parts.slice(1).join('/');
73+
}
74+
return key;
75+
}, [channelData, url]);
76+
77+
const { data: presignedUrl } = useOAIQuery({
78+
path: '/api/admin/projects/{projectId}/channels/{channelId}/image-download-url',
79+
variables: { channelId, projectId, imageKey },
80+
queryOptions: {
81+
enabled: Boolean(
82+
imageKey && channelData?.imageConfig?.enablePresignedUrlDownload,
83+
),
84+
},
85+
});
86+
87+
return (
88+
<Image
89+
src={presignedUrl ?? url}
90+
alt={presignedUrl ?? url}
91+
className="cursor-pointer object-cover"
92+
fill
93+
/>
94+
);
95+
};
96+
97+
export default FeedbackImage;

apps/web/src/shared/ui/image-preview-button.tsx

Lines changed: 4 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
* under the License.
1515
*/
1616

17-
import { useEffect, useMemo, useState } from 'react';
18-
import Image from 'next/image';
19-
import { useRouter } from 'next/router';
17+
import { useEffect, useState } from 'react';
2018
import { useTranslation } from 'next-i18next';
2119
import { FreeMode, Navigation, Thumbs } from 'swiper/modules';
2220
import { Swiper, SwiperSlide } from 'swiper/react';
@@ -33,7 +31,7 @@ import {
3331
DialogTrigger,
3432
} from '@ufb/react';
3533

36-
import { useOAIQuery } from '@/shared';
34+
import FeedbackImage from './feedback-image';
3735

3836
interface IProps extends React.PropsWithChildren {
3937
urls: string[];
@@ -76,7 +74,7 @@ const ImagePreviewButton: React.FC<IProps> = (props) => {
7674
>
7775
{urls.map((url) => (
7876
<SwiperSlide key={url} className="relative">
79-
<PresignedURLImage url={url} />
77+
<FeedbackImage url={url} />
8078
</SwiperSlide>
8179
))}
8280
</Swiper>
@@ -94,7 +92,7 @@ const ImagePreviewButton: React.FC<IProps> = (props) => {
9492
key={url}
9593
className="rounded-8 bg-neutral-secondary relative overflow-hidden"
9694
>
97-
<PresignedURLImage url={url} />
95+
<FeedbackImage url={url} />
9896
</SwiperSlide>
9997
))}
10098
</Swiper>
@@ -106,78 +104,5 @@ const ImagePreviewButton: React.FC<IProps> = (props) => {
106104
</Dialog>
107105
);
108106
};
109-
interface IPresignedURLImageProps {
110-
url: string;
111-
}
112-
const PresignedURLImage = ({ url }: IPresignedURLImageProps) => {
113-
const router = useRouter();
114-
const projectId = Number(router.query.projectId);
115-
const channelId = Number(router.query.channelId);
116-
117-
const { data: channelData } = useOAIQuery({
118-
path: '/api/admin/projects/{projectId}/channels/{channelId}',
119-
variables: { channelId, projectId },
120-
queryOptions: {
121-
enabled:
122-
router.isReady &&
123-
Number.isFinite(projectId) &&
124-
Number.isFinite(channelId),
125-
},
126-
});
127-
128-
const imageKey = useMemo(() => {
129-
if (!channelData?.imageConfig?.enablePresignedUrlDownload) return url;
130-
131-
let parsed: URL | null = null;
132-
try {
133-
parsed = new URL(
134-
url,
135-
typeof window !== 'undefined' ?
136-
window.location.href
137-
: 'http://localhost',
138-
);
139-
} catch {
140-
return url;
141-
}
142-
if (!/^https?:$/.test(parsed.protocol)) return url;
143-
144-
const rawPath = parsed.pathname.replace(/^\/+/, '');
145-
let key = rawPath;
146-
try {
147-
key = decodeURIComponent(rawPath);
148-
} catch {
149-
/* keep raw */
150-
}
151-
152-
const host = parsed.hostname;
153-
const parts = key.split('/', 2);
154-
if (
155-
(host === 's3.amazonaws.com' || host.startsWith('s3.')) &&
156-
parts.length >= 2
157-
) {
158-
return parts.slice(1).join('/');
159-
}
160-
return key;
161-
}, [channelData, url]);
162-
163-
const { data: presignedUrl } = useOAIQuery({
164-
path: '/api/admin/projects/{projectId}/channels/{channelId}/image-download-url',
165-
variables: { channelId, projectId, imageKey },
166-
queryOptions: {
167-
enabled: Boolean(
168-
imageKey && channelData?.imageConfig?.enablePresignedUrlDownload,
169-
),
170-
},
171-
});
172-
173-
return (
174-
<Image
175-
src={presignedUrl ?? url}
176-
alt={presignedUrl ?? url}
177-
fill
178-
className="cursor-pointer object-cover"
179-
/>
180-
);
181-
};
182107

183108
export default ImagePreviewButton;

apps/web/src/shared/ui/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,7 @@ export { default as SheetDetailTable } from './sheet-detail-table.ui';
5151
export * from './table-filter-popover';
5252
export { default as NoProjectDialogInProjectCreation } from './no-project-dialog-in-project-creation.ui';
5353
export { default as InfiniteScrollArea } from './infinite-scroll-area.ui';
54+
export { default as FeedbackImage } from './feedback-image';
55+
5456
export * from './card.ui';
5557
export * from './slider.ui';

apps/web/src/shared/ui/sheet-detail-table.ui.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* License for the specific language governing permissions and limitations
1414
* under the License.
1515
*/
16-
import Image from 'next/image';
1716
import { useRouter } from 'next/router';
1817
import dayjs from 'dayjs';
1918
import Linkify from 'linkify-react';
@@ -39,6 +38,7 @@ import type { BadgeColor } from '../constants/color-map';
3938
import { BADGE_COLOR_MAP } from '../constants/color-map';
4039
import { useOAIMutation, usePermissions } from '../lib';
4140
import { cn } from '../utils';
41+
import FeedbackImage from './feedback-image';
4242
import ImagePreviewButton from './image-preview-button';
4343
import {
4444
DatePicker,
@@ -201,7 +201,7 @@ const SheetDetailTable = (props: Props) => {
201201
className="bg-neutral-tertiary relative h-16 w-16 overflow-hidden rounded"
202202
key={index}
203203
>
204-
<Image src={v} alt={v} fill />
204+
<FeedbackImage url={v} />
205205
<div className="absolute inset-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 items-center justify-center">
206206
<ImagePreviewButton
207207
urls={value as string[]}
@@ -363,7 +363,7 @@ const SheetDetailTable = (props: Props) => {
363363
className="bg-neutral-tertiary relative h-16 w-16 overflow-hidden rounded"
364364
key={v}
365365
>
366-
<Image src={v} alt={v} fill />
366+
<FeedbackImage url={v} />
367367
<div className="absolute inset-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-1">
368368
<ImagePreviewButton
369369
urls={value as string[]}

0 commit comments

Comments
 (0)