-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #40 from takahiroanno2024/feature/20-fullscreen-tools
Fixes #20 - Optimize Fullscreen Menu
- Loading branch information
Showing
14 changed files
with
422 additions
and
402 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
scatter/next-app/components/DesktopFullscreenFavorites.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import {BookmarkCheckIcon, XIcon} from 'lucide-react' | ||
import React from 'react' | ||
import {ColorFunc} from '@/hooks/useClusterColor' | ||
import {Translator} from '@/hooks/useTranslatorAndReplacements' | ||
import {Cluster, FavoritePoint} from '@/types' | ||
|
||
type Props = { | ||
favorites: FavoritePoint[] | ||
removeFavorite: (fav: FavoritePoint) => void | ||
clusters: Cluster[] | ||
translator: Translator | ||
color: ColorFunc | ||
onlyCluster?: string | ||
onClose: () => void | ||
} | ||
|
||
export function DesktopFullscreenFavorites({ | ||
favorites, | ||
removeFavorite, | ||
clusters, | ||
translator, | ||
color, | ||
onlyCluster, | ||
onClose | ||
}: Props) { | ||
return ( | ||
<div | ||
className="absolute top-0 right-0 w-[400px] p-4 bg-gray-100 overflow-y-auto z-10 h-full shadow-md" | ||
> | ||
<div className="flex justify-between items-center mb-4 h-[32px]"> | ||
<h2 className="text-md font-bold"> | ||
{translator.t('お気に入り一覧')} | ||
</h2> | ||
<button onClick={onClose}> | ||
<XIcon/> | ||
</button> | ||
</div> | ||
{favorites.length === 0 ? ( | ||
<p>{translator.t('お気に入りがありません')}</p> | ||
) : ( | ||
<ul> | ||
{favorites.map((fav) => { | ||
// クラスタ情報を取得 | ||
const cluster = clusters.find((c) => c.cluster_id === fav.cluster_id) | ||
return ( | ||
<li | ||
key={fav.arg_id} | ||
className="mb-2 p-2 bg-white rounded shadow flex flex-col" | ||
> | ||
<div className="flex items-center justify-between"> | ||
{/* クラスタラベル */} | ||
<h3 | ||
className="font-semibold text-md" | ||
style={{ | ||
color: cluster ? color(cluster.cluster_id, onlyCluster) : '#000', | ||
}} | ||
> | ||
{translator.t(cluster?.cluster || 'クラスタ')} | ||
</h3> | ||
|
||
{/* お気に入りボタン */} | ||
<button | ||
onClick={() => removeFavorite(fav)} | ||
className="text-amber-500 text-lg focus:outline-none ml-2" | ||
> | ||
<BookmarkCheckIcon/> | ||
</button> | ||
</div> | ||
|
||
{/* 引用部分を100文字に制限 */} | ||
<p className="text-md text-gray-700 mt-1"> | ||
{truncateText(fav.argument, 100)} | ||
</p> | ||
</li> | ||
) | ||
})} | ||
</ul> | ||
)} | ||
</div> | ||
) | ||
} | ||
|
||
function truncateText(text: string, maxLength: number) { | ||
if (text.length <= maxLength) return text | ||
return text.slice(0, maxLength) + '...' | ||
} |
135 changes: 135 additions & 0 deletions
135
scatter/next-app/components/DesktopFullscreenFilter.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import {XIcon} from 'lucide-react' | ||
import React from 'react' | ||
import {Translator} from '@/hooks/useTranslatorAndReplacements' | ||
import {Argument, PropertyMap} from '@/types' | ||
|
||
type Props = { | ||
translator: Translator | ||
onClose: () => void | ||
highlightText: string | ||
setHighlightText: React.Dispatch<React.SetStateAction<string>> | ||
propertyMap: PropertyMap | ||
propertyFilter: { [key: string]: string } | ||
setPropertyFilter: React.Dispatch<React.SetStateAction<{ [key: string]: string }>> | ||
dataHasVotes: boolean | ||
minVotes: number | ||
setMinVotes: React.Dispatch<React.SetStateAction<number>> | ||
minConsensus: number | ||
setMinConsensus: React.Dispatch<React.SetStateAction<number>> | ||
voteFilter: { | ||
total: number | ||
filtered: number | ||
filter: (arg: Argument) => boolean | ||
} | ||
} | ||
|
||
export function DesktopFullscreenFilter(props: Props) { | ||
const {t} = props.translator | ||
return ( | ||
<div | ||
className="absolute top-0 left-0 w-[400px] p-4 bg-gray-100 overflow-y-auto z-10 h-full shadow-md" | ||
> | ||
{/* Header */} | ||
<div className="flex justify-between items-center mb-4 h-[32px]"> | ||
<h2 className="text-md font-bold"> | ||
{t('toolsFilterSettings')} | ||
</h2> | ||
<button onClick={props.onClose}> | ||
<XIcon/> | ||
</button> | ||
</div> | ||
{/* Search */} | ||
<input | ||
type="text" | ||
placeholder={t('検索')} | ||
value={props.highlightText} | ||
onChange={(e) => props.setHighlightText(e.target.value)} | ||
className="w-full mb-2 p-2 border rounded" | ||
/> | ||
<dl className={'mb-2'}> | ||
{Object.entries(props.propertyMap).map(([propKey, propValues]) => { | ||
const uniqueValues = Array.from(new Set(Object.values(propValues))) | ||
uniqueValues.sort() // ABC順にソート | ||
return ( | ||
<div key={propKey}> | ||
<dt>{propKey}</dt> | ||
<dd> | ||
<select | ||
value={props.propertyFilter[propKey] ?? ''} | ||
onChange={(e) => { | ||
// propKeyごとの選択値を状態管理する必要あり | ||
props.setPropertyFilter((prev) => ({ | ||
...prev, | ||
[propKey]: e.target.value, | ||
})) | ||
}} | ||
className="border p-1 rounded w-full" | ||
> | ||
<option value="">{t('(すべて)')}</option> | ||
{uniqueValues.map((val) => ( | ||
<option key={val} value={val}> | ||
{val} | ||
</option> | ||
))} | ||
</select> | ||
</dd> | ||
</div> | ||
) | ||
})} | ||
</dl> | ||
|
||
{/* Votes */} | ||
{props.dataHasVotes && ( | ||
<div> | ||
<div className="flex justify-between"> | ||
<button className="inline-block text-left"> | ||
{t('Votes')} {'>'}{' '} | ||
<span className="inline-block w-10">{props.minVotes}</span> | ||
</button> | ||
<input | ||
className="inline-block w-[200px] mr-2" | ||
id="min-votes-slider" | ||
type="range" | ||
min="0" | ||
max="50" | ||
value={props.minVotes} | ||
onInput={(e) => { | ||
props.setMinVotes( | ||
parseInt( | ||
(e.target as HTMLInputElement).value | ||
) | ||
) | ||
}} | ||
/> | ||
</div> | ||
<div className="flex justify-between"> | ||
<button className="inline-block text-left"> | ||
{t('Consensus')} {'>'}{' '} | ||
<span className="inline-block w-10">{props.minConsensus}%</span> | ||
</button> | ||
<input | ||
className="inline-block w-[200px] mr-2" | ||
id="min-consensus-slider" | ||
type="range" | ||
min="50" | ||
max="100" | ||
value={props.minConsensus} | ||
onInput={(e) => { | ||
props.setMinConsensus( | ||
parseInt( | ||
(e.target as HTMLInputElement).value | ||
) | ||
) | ||
}} | ||
/> | ||
</div> | ||
<div className="text-sm opacity-70"> | ||
{t('Showing')} {props.voteFilter.filtered}/ | ||
{props.voteFilter.total} {t('arguments')} | ||
</div> | ||
</div> | ||
)} | ||
|
||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { | ||
AtSignIcon, | ||
BookMarkedIcon, | ||
ChartPieIcon, | ||
Minimize2Icon, | ||
ScanSearchIcon, | ||
SlidersHorizontalIcon, | ||
TagIcon | ||
} from 'lucide-react' | ||
import React from 'react' | ||
import {Translator} from '@/hooks/useTranslatorAndReplacements' | ||
|
||
type Props = { | ||
canFilter: boolean // !!propertyMap | ||
zoomReset: boolean | (() => void) | ||
translator: Translator | ||
exitFullScreen: () => void // back | ||
showSettings: boolean | ||
setShowSettings: React.Dispatch<React.SetStateAction<boolean>> | ||
showLabels: boolean | ||
setShowLabels: React.Dispatch<React.SetStateAction<boolean>> | ||
showTitle: boolean | ||
setShowTitle: React.Dispatch<React.SetStateAction<boolean>> | ||
showRatio: boolean | ||
setShowRatio: React.Dispatch<React.SetStateAction<boolean>> | ||
showFavorites: boolean | ||
setShowFavorites: React.Dispatch<React.SetStateAction<boolean>> | ||
} | ||
|
||
export function DesktopFullscreenTools(props: Props) { | ||
const {t} = props.translator | ||
return ( | ||
<div className="absolute top-0 w-full p-2" style={{backgroundColor: 'rgba(255, 255, 255, 0.5)'}}> | ||
<div className={'flex justify-between items-center'}> | ||
<div className={'flex'}> | ||
{props.canFilter && ( | ||
<button className="w-[80px] m-1 flex flex-col items-center" onClick={() => props.setShowSettings(x => !x)}> | ||
<SlidersHorizontalIcon className={props.showSettings ? 'text-blue-500' : 'text-gray-500'}/> | ||
<p className={'text-xs text-gray-700'}>{t('toolsFilterSettings')}</p> | ||
</button> | ||
)} | ||
<button className="w-[80px] m-1 flex flex-col items-center" onClick={() => props.setShowLabels(x => !x)}> | ||
<TagIcon className={props.showLabels ? 'text-blue-500' : 'text-gray-500'}/> | ||
<p className={'text-xs text-gray-700'}>{t('toolsDisplayLabels')}</p> | ||
</button> | ||
<button className="w-[80px] m-1 flex flex-col items-center" onClick={() => props.setShowTitle(x => !x)}> | ||
<AtSignIcon className={props.showTitle ? 'text-blue-500' : 'text-gray-500'}/> | ||
<p className={'text-xs text-gray-700'}>{t('toolsDisplayTitle')}</p> | ||
</button> | ||
<button className="w-[80px] m-1 flex flex-col items-center" onClick={() => props.setShowRatio(x => !x)}> | ||
<ChartPieIcon className={props.showRatio ? 'text-blue-500' : 'text-gray-500'}/> | ||
<p className={'text-xs text-gray-700'}>{t('toolsDisplayPercentage')}</p> | ||
</button> | ||
<button className="w-[80px] m-1 flex flex-col items-center" | ||
onClick={() => typeof props.zoomReset === 'function' && props.zoomReset()}> | ||
<ScanSearchIcon className={props.zoomReset ? 'text-blue-500' : 'text-gray-500'}/> | ||
<p className={'text-xs text-gray-700'}>{t('toolsResetPosition')}</p> | ||
</button> | ||
</div> | ||
<div className={'flex justify-between items-center'}> | ||
<button className="w-[80px] m-1 flex flex-col items-center" onClick={() => props.setShowFavorites(x => !x)}> | ||
<BookMarkedIcon className={'text-gray-700'}/> | ||
<p className={'text-xs text-gray-700'}>{t('toolsDisplayFavorites')}</p> | ||
</button> | ||
<button className="w-[80px] m-1 flex flex-col items-center" onClick={props.exitFullScreen}> | ||
<Minimize2Icon className={'text-gray-700'}/> | ||
<p className={'text-xs text-gray-700'}>{t('toolsExitFullScreen')}</p> | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} |
Oops, something went wrong.