sync: update from private repo (e62f5c7)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-11 01:52:48 +00:00
parent 000a2474aa
commit d061ad08d8
237 changed files with 8441 additions and 5549 deletions
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { AmazonData } from './types';
function StarRating({ rating }: { rating: number }) {
@@ -10,6 +11,7 @@ function StarRating({ rating }: { rating: number }) {
}
export function AmazonProductsCard({ data, onExpand }: { data: AmazonData; onExpand: () => void }) {
const { t } = useTranslation('embed');
const { query, products } = data;
return (
@@ -17,8 +19,8 @@ export function AmazonProductsCard({ data, onExpand }: { data: AmazonData; onExp
{/* Header */}
<div className="flex items-center gap-2 mb-3">
<span className="text-sm">&#128722;</span>
<span className="font-semibold text-slate-700" style={{ fontSize: 13 }}>Amazon : {query}</span>
<span className="text-slate-400 ml-auto" style={{ fontSize: 11 }}>{products.length}</span>
<span className="font-semibold text-slate-700" style={{ fontSize: 13 }}>{t('searchResults.amazon', { query })}</span>
<span className="text-slate-400 ml-auto" style={{ fontSize: 11 }}>{t('count', { count: products.length })}</span>
</div>
{/* Horizontal scroll cards */}
@@ -59,7 +61,7 @@ export function AmazonProductsCard({ data, onExpand }: { data: AmazonData; onExp
className="text-blue-500 hover:text-blue-700 dark:hover:text-blue-300 cursor-pointer bg-transparent border-none"
style={{ fontSize: 11 }}
>
&#9660;
&#9660; {t('expand')}
</button>
</div>
</div>
@@ -1,6 +1,8 @@
import { useTranslation } from 'react-i18next';
import type { AmazonData } from './types';
function StarRating({ rating, reviewCount }: { rating: number; reviewCount?: number }) {
const { t } = useTranslation('embed');
const full = Math.floor(rating);
const half = rating - full >= 0.5;
const stars: string[] = [];
@@ -9,18 +11,19 @@ function StarRating({ rating, reviewCount }: { rating: number; reviewCount?: num
return (
<span className="text-amber-400 text-sm">
{stars.join('')} {rating.toFixed(1)}
{reviewCount != null && <span className="text-slate-400 text-xs ml-1">({reviewCount.toLocaleString()})</span>}
{reviewCount != null && <span className="text-slate-400 text-xs ml-1">{t('reviewCount', { count: reviewCount.toLocaleString() })}</span>}
</span>
);
}
export function AmazonProductsDetail({ data }: { data: AmazonData }) {
const { t } = useTranslation('embed');
const { query, products } = data;
return (
<div className="p-6">
<h2 className="text-lg font-bold text-slate-800 mb-4">
&#128722; Amazon : {query}
&#128722; {t('searchResults.amazon', { query })}
</h2>
<div className="space-y-6">
@@ -60,7 +63,7 @@ export function AmazonProductsDetail({ data }: { data: AmazonData }) {
rel="noopener noreferrer"
className="inline-flex items-center gap-1 px-3 py-1.5 bg-amber-400 hover:bg-amber-500 text-slate-900 text-xs font-semibold rounded-lg no-underline transition-colors"
>
Amazon
{t('viewOn.amazon')}
</a>
<a
href={p.keepaDetailUrl}
@@ -68,7 +71,7 @@ export function AmazonProductsDetail({ data }: { data: AmazonData }) {
rel="noopener noreferrer"
className="inline-flex items-center gap-1 px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-semibold rounded-lg no-underline transition-colors"
>
Keepa
{t('viewOn.keepa')}
</a>
</div>
</div>
@@ -76,10 +79,10 @@ export function AmazonProductsDetail({ data }: { data: AmazonData }) {
{/* Keepa price graph */}
<div className="mt-4 bg-slate-50 rounded-lg p-3">
<div className="text-xs text-slate-500 mb-2">&#128200; (Keepa)</div>
<div className="text-xs text-slate-500 mb-2">&#128200; {t('priceHistory')}</div>
<img
src={p.keepaGraphUrl}
alt={`${p.title} 価格推移`}
alt={t('priceHistoryAlt', { title: p.title })}
className="w-full rounded"
loading="lazy"
/>
+3 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useCallback, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { useBackdropClose } from '../../lib/useBackdropClose';
interface EmbedModalProps {
@@ -9,6 +10,7 @@ interface EmbedModalProps {
}
export function EmbedModal({ open, onClose, children }: EmbedModalProps) {
const { t } = useTranslation('embed');
const backdrop = useBackdropClose(onClose);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
@@ -56,7 +58,7 @@ export function EmbedModal({ open, onClose, children }: EmbedModalProps) {
rounded-full text-slate-500 hover:text-slate-700
transition-colors cursor-pointer border-none text-lg
"
aria-label="閉じる"
aria-label={t('close')}
>
&#10005;
</button>
+5 -3
View File
@@ -1,6 +1,8 @@
import { useTranslation } from 'react-i18next';
import type { MapData } from './types';
export function MapPlacesCard({ data, onExpand }: { data: MapData; onExpand: () => void }) {
const { t } = useTranslation('embed');
const { query, places } = data;
return (
@@ -8,8 +10,8 @@ export function MapPlacesCard({ data, onExpand }: { data: MapData; onExpand: ()
{/* Header */}
<div className="flex items-center gap-2 mb-3">
<span className="text-sm">&#128205;</span>
<span className="font-semibold text-slate-700" style={{ fontSize: 13 }}>: {query}</span>
<span className="text-slate-400 ml-auto" style={{ fontSize: 11 }}>{places.length}</span>
<span className="font-semibold text-slate-700" style={{ fontSize: 13 }}>{t('searchResults.map', { query })}</span>
<span className="text-slate-400 ml-auto" style={{ fontSize: 11 }}>{t('count', { count: places.length })}</span>
</div>
{/* Place list */}
@@ -35,7 +37,7 @@ export function MapPlacesCard({ data, onExpand }: { data: MapData; onExpand: ()
className="text-blue-500 hover:text-blue-700 dark:hover:text-blue-300 cursor-pointer bg-transparent border-none"
style={{ fontSize: 11 }}
>
&#9660;
&#9660; {t('expandMap')}
</button>
</div>
</div>
+4 -2
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import type { MapData } from './types';
// Leaflet CDN を動的にロードする
@@ -33,6 +34,7 @@ function loadLeaflet(): Promise<void> {
declare const L: typeof import('leaflet');
export function MapPlacesDetail({ data }: { data: MapData }) {
const { t } = useTranslation('embed');
const { query, places } = data;
const mapRef = useRef<HTMLDivElement>(null);
const mapInstanceRef = useRef<import('leaflet').Map | null>(null);
@@ -85,7 +87,7 @@ export function MapPlacesDetail({ data }: { data: MapData }) {
return (
<div className="p-6">
<h2 className="text-lg font-bold text-slate-800 mb-4">
&#128205; : {query}
&#128205; {t('searchResults.map', { query })}
</h2>
{/* Leaflet map */}
@@ -115,7 +117,7 @@ export function MapPlacesDetail({ data }: { data: MapData }) {
rel="noopener noreferrer"
className="inline-flex items-center gap-1 px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-semibold rounded-lg no-underline transition-colors"
>
OpenStreetMap
{t('viewOn.osm')}
</a>
</div>
))}
+5 -3
View File
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { XPostData } from './types';
function formatNumber(n: number): string {
@@ -7,6 +8,7 @@ function formatNumber(n: number): string {
}
export function XPostsCard({ data, onExpand }: { data: XPostData; onExpand: () => void }) {
const { t } = useTranslation('embed');
const { query, posts } = data;
return (
@@ -14,8 +16,8 @@ export function XPostsCard({ data, onExpand }: { data: XPostData; onExpand: () =
{/* Header */}
<div className="flex items-center gap-2 mb-3">
<span className="font-bold text-slate-800" style={{ fontSize: 14 }}>&#120143;</span>
<span className="font-semibold text-slate-700" style={{ fontSize: 13 }}>X : {query}</span>
<span className="text-slate-400 ml-auto" style={{ fontSize: 11 }}>{posts.length}</span>
<span className="font-semibold text-slate-700" style={{ fontSize: 13 }}>{t('searchResults.x', { query })}</span>
<span className="text-slate-400 ml-auto" style={{ fontSize: 11 }}>{t('count', { count: posts.length })}</span>
</div>
{/* Post list */}
@@ -58,7 +60,7 @@ export function XPostsCard({ data, onExpand }: { data: XPostData; onExpand: () =
className="text-blue-500 hover:text-blue-700 dark:hover:text-blue-300 cursor-pointer bg-transparent border-none"
style={{ fontSize: 11 }}
>
&#9660;
&#9660; {t('expand')}
</button>
</div>
</div>
+8 -6
View File
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { XPostData } from './types';
function formatNumber(n: number): string {
@@ -18,12 +19,13 @@ function formatDate(iso: string): string {
}
export function XPostsDetail({ data }: { data: XPostData }) {
const { t } = useTranslation('embed');
const { query, posts } = data;
return (
<div className="p-6">
<h2 className="text-lg font-bold text-slate-800 mb-4">
<span style={{ fontSize: 20 }}>&#120143;</span> X : {query}
<span style={{ fontSize: 20 }}>&#120143;</span> {t('searchResults.x', { query })}
</h2>
<div className="space-y-4">
@@ -54,10 +56,10 @@ export function XPostsDetail({ data }: { data: XPostData }) {
{/* Metrics */}
<div className="flex gap-4 text-slate-400 mb-3" style={{ fontSize: 12 }}>
<span title="いいね">&#9829; {formatNumber(p.likes)}</span>
<span title="リポスト">&#128257; {formatNumber(p.retweets)}</span>
<span title="返信">&#128172; {formatNumber(p.replies)}</span>
<span title="表示">&#128065; {formatNumber(p.views)}</span>
<span title={t('metrics.likes')}>&#9829; {formatNumber(p.likes)}</span>
<span title={t('metrics.retweets')}>&#128257; {formatNumber(p.retweets)}</span>
<span title={t('metrics.replies')}>&#128172; {formatNumber(p.replies)}</span>
<span title={t('metrics.views')}>&#128065; {formatNumber(p.views)}</span>
</div>
{/* Link */}
@@ -67,7 +69,7 @@ export function XPostsDetail({ data }: { data: XPostData }) {
rel="noopener noreferrer"
className="inline-flex items-center gap-1 px-3 py-1.5 bg-slate-900 hover:bg-slate-700 text-white text-xs font-semibold rounded-lg no-underline transition-colors"
>
X
{t('viewOn.x')}
</a>
</div>
))}
@@ -1,6 +1,8 @@
import { useTranslation } from 'react-i18next';
import type { YouTubeData } from './types';
export function YouTubeVideosCard({ data, onExpand }: { data: YouTubeData; onExpand: () => void }) {
const { t } = useTranslation('embed');
const { query, videos } = data;
return (
@@ -8,8 +10,8 @@ export function YouTubeVideosCard({ data, onExpand }: { data: YouTubeData; onExp
{/* Header */}
<div className="flex items-center gap-2 mb-3">
<span className="text-sm">&#9654;</span>
<span className="font-semibold text-slate-700" style={{ fontSize: 13 }}>YouTube : {query}</span>
<span className="text-slate-400 ml-auto" style={{ fontSize: 11 }}>{videos.length}</span>
<span className="font-semibold text-slate-700" style={{ fontSize: 13 }}>{t('searchResults.youtube', { query })}</span>
<span className="text-slate-400 ml-auto" style={{ fontSize: 11 }}>{t('count', { count: videos.length })}</span>
</div>
{/* Horizontal scroll thumbnails */}
@@ -62,7 +64,7 @@ export function YouTubeVideosCard({ data, onExpand }: { data: YouTubeData; onExp
className="text-blue-500 hover:text-blue-700 dark:hover:text-blue-300 cursor-pointer bg-transparent border-none"
style={{ fontSize: 11 }}
>
&#9660;
&#9660; {t('expand')}
</button>
</div>
</div>
@@ -1,12 +1,14 @@
import { useTranslation } from 'react-i18next';
import type { YouTubeData } from './types';
export function YouTubeVideosDetail({ data }: { data: YouTubeData }) {
const { t } = useTranslation('embed');
const { query, videos } = data;
return (
<div className="p-6">
<h2 className="text-lg font-bold text-slate-800 mb-4">
&#9654; YouTube : {query}
&#9654; {t('searchResults.youtube', { query })}
</h2>
<div className="space-y-6">
@@ -55,7 +57,7 @@ export function YouTubeVideosDetail({ data }: { data: YouTubeData }) {
rel="noopener noreferrer"
className="inline-flex items-center gap-1 px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white text-xs font-semibold rounded-lg no-underline transition-colors"
>
YouTube
{t('viewOn.youtube')}
</a>
</div>
</div>