Skip to content

Commit 76cb242

Browse files
committed
Add sticky save button to setting page
1 parent a269d4f commit 76cb242

2 files changed

Lines changed: 107 additions & 84 deletions

File tree

src/app/components/Button.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { cx } from '~/utils'
22
import { ButtonHTMLAttributes, FC, ReactNode } from 'react'
33
import { BeatLoader } from 'react-spinners'
4+
import React from 'react'
5+
import { motion } from 'framer-motion'
46

57
export interface Props {
68
text: string
@@ -13,11 +15,12 @@ export interface Props {
1315
icon?: ReactNode
1416
}
1517

16-
const Button: FC<Props> = (props) => {
18+
const Button = React.forwardRef<HTMLButtonElement, Props>((props, ref) => {
1719
const size = props.size || 'normal'
1820
const type = props.type || 'button'
1921
return (
2022
<button
23+
ref={ref}
2124
type={type}
2225
className={cx(
2326
size === 'normal' && 'text-base font-medium px-6 py-[5px] rounded-full',
@@ -38,6 +41,10 @@ const Button: FC<Props> = (props) => {
3841
)}
3942
</button>
4043
)
41-
}
44+
})
45+
46+
Button.displayName = 'Button'
4247

4348
export default Button
49+
50+
export const MotionButton = motion(Button)

src/app/pages/SettingPage.tsx

Lines changed: 98 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { useBlocker } from '@tanstack/react-router'
2+
import { motion } from 'framer-motion'
23
import { FC, PropsWithChildren, useCallback, useEffect, useState } from 'react'
34
import toast, { Toaster } from 'react-hot-toast'
45
import { useTranslation } from 'react-i18next'
56
import { BiExport, BiImport } from 'react-icons/bi'
67
import Browser from 'webextension-polyfill'
7-
import Button from '~app/components/Button'
8+
import Button, { MotionButton } from '~app/components/Button'
89
import RadioGroup from '~app/components/RadioGroup'
910
import Select from '~app/components/Select'
1011
import ChatGPTAPISettings from '~app/components/Settings/ChatGPTAPISettings'
@@ -93,6 +94,7 @@ function SettingPage() {
9394
apiHost = undefined
9495
}
9596
await updateUserConfig({ ...userConfig!, openaiApiHost: apiHost })
97+
setDirty(false)
9698
toast.success('Saved')
9799
setTimeout(() => location.reload(), 500)
98100
}, [userConfig])
@@ -116,94 +118,108 @@ function SettingPage() {
116118

117119
return (
118120
<PagePanel title={`${t('Settings')} (v${getVersion()})`}>
119-
<div className="flex flex-col gap-5 mt-3">
120-
<div>
121-
<p className="font-bold mb-1 text-lg">{t('Export/Import All Data')}</p>
122-
<p className="mb-3 opacity-80">{t('Data includes all your settings, chat histories, and local prompts')}</p>
123-
<div className="flex flex-row gap-3">
124-
<Button size="small" text={t('Export')} icon={<BiExport />} onClick={exportData} />
125-
<Button size="small" text={t('Import')} icon={<BiImport />} onClick={importData} />
126-
</div>
127-
</div>
128-
<div className="flex flex-col gap-2">
129-
<p className="font-bold text-lg">{t('Shortcut to open this app')}</p>
130-
<div className="flex flex-row gap-2 items-center">
131-
{shortcuts.length > 0 && (
132-
<div className="flex flex-row gap-1">
133-
{shortcuts.map((s) => (
134-
<KDB key={s} text={s} />
135-
))}
136-
</div>
137-
)}
138-
<Button text={t('Change shortcut')} size="small" onClick={openShortcutPage} />
121+
<div className="flex flex-row mt-3 mb-5 gap-3">
122+
<div className="flex flex-col gap-5">
123+
<div>
124+
<p className="font-bold mb-1 text-lg">{t('Export/Import All Data')}</p>
125+
<p className="mb-3 opacity-80">{t('Data includes all your settings, chat histories, and local prompts')}</p>
126+
<div className="flex flex-row gap-3">
127+
<Button size="small" text={t('Export')} icon={<BiExport />} onClick={exportData} />
128+
<Button size="small" text={t('Import')} icon={<BiImport />} onClick={importData} />
129+
</div>
139130
</div>
140-
</div>
141-
<div>
142-
<p className="font-bold mb-2 text-lg">{t('Startup page')}</p>
143-
<div className="w-[200px]">
144-
<Select
145-
options={[
146-
{ name: 'All-In-One', value: ALL_IN_ONE_PAGE_ID },
147-
...Object.entries(CHATBOTS).map(([botId, bot]) => ({ name: bot.name, value: botId })),
148-
]}
149-
value={userConfig.startupPage}
150-
onChange={(v) => updateConfigValue({ startupPage: v })}
151-
/>
131+
<div className="flex flex-col gap-2">
132+
<p className="font-bold text-lg">{t('Shortcut to open this app')}</p>
133+
<div className="flex flex-row gap-2 items-center">
134+
{shortcuts.length > 0 && (
135+
<div className="flex flex-row gap-1">
136+
{shortcuts.map((s) => (
137+
<KDB key={s} text={s} />
138+
))}
139+
</div>
140+
)}
141+
<Button text={t('Change shortcut')} size="small" onClick={openShortcutPage} />
142+
</div>
152143
</div>
153-
</div>
154-
<div className="flex flex-col gap-2">
155-
<p className="font-bold text-lg">{t('Chatbots')}</p>
156-
<EnabledBotsSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
157-
</div>
158-
<ChatBotSettingPanel title="ChatGPT">
159-
<RadioGroup
160-
options={Object.entries(ChatGPTMode).map(([k, v]) => ({ label: `${k} ${t('Mode')}`, value: v }))}
161-
value={userConfig.chatgptMode}
162-
onChange={(v) => updateConfigValue({ chatgptMode: v as ChatGPTMode })}
163-
/>
164-
{userConfig.chatgptMode === ChatGPTMode.API ? (
165-
<ChatGPTAPISettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
166-
) : userConfig.chatgptMode === ChatGPTMode.Azure ? (
167-
<ChatGPTAzureSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
168-
) : userConfig.chatgptMode === ChatGPTMode.Poe ? (
169-
<ChatGPTPoeSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
170-
) : userConfig.chatgptMode === ChatGPTMode.OpenRouter ? (
171-
<ChatGPTOpenRouterSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
172-
) : (
173-
<ChatGPWebSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
174-
)}
175-
</ChatBotSettingPanel>
176-
<ChatBotSettingPanel title="Claude">
177-
<RadioGroup
178-
options={Object.entries(ClaudeMode).map(([k, v]) => ({ label: `${k} ${t('Mode')}`, value: v }))}
179-
value={userConfig.claudeMode}
180-
onChange={(v) => updateConfigValue({ claudeMode: v as ClaudeMode })}
181-
/>
182-
{userConfig.claudeMode === ClaudeMode.API ? (
183-
<ClaudeAPISettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
184-
) : userConfig.claudeMode === ClaudeMode.Webapp ? (
185-
<ClaudeWebappSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
186-
) : userConfig.claudeMode === ClaudeMode.OpenRouter ? (
187-
<ClaudeOpenRouterSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
188-
) : (
189-
<ClaudePoeSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
190-
)}
191-
</ChatBotSettingPanel>
192-
<ChatBotSettingPanel title="Bing">
193-
<div className="flex flex-row gap-3 items-center justify-between w-[250px]">
194-
<p className="font-medium text-base">{t('Chat style')}</p>
195-
<div className="w-[150px]">
144+
<div>
145+
<p className="font-bold mb-2 text-lg">{t('Startup page')}</p>
146+
<div className="w-[200px]">
196147
<Select
197-
options={BING_STYLE_OPTIONS}
198-
value={userConfig.bingConversationStyle}
199-
onChange={(v) => updateConfigValue({ bingConversationStyle: v })}
148+
options={[
149+
{ name: 'All-In-One', value: ALL_IN_ONE_PAGE_ID },
150+
...Object.entries(CHATBOTS).map(([botId, bot]) => ({ name: bot.name, value: botId })),
151+
]}
152+
value={userConfig.startupPage}
153+
onChange={(v) => updateConfigValue({ startupPage: v })}
200154
/>
201155
</div>
202156
</div>
203-
</ChatBotSettingPanel>
157+
<div className="flex flex-col gap-2">
158+
<p className="font-bold text-lg">{t('Chatbots')}</p>
159+
<EnabledBotsSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
160+
</div>
161+
<ChatBotSettingPanel title="ChatGPT">
162+
<RadioGroup
163+
options={Object.entries(ChatGPTMode).map(([k, v]) => ({ label: `${k} ${t('Mode')}`, value: v }))}
164+
value={userConfig.chatgptMode}
165+
onChange={(v) => updateConfigValue({ chatgptMode: v as ChatGPTMode })}
166+
/>
167+
{userConfig.chatgptMode === ChatGPTMode.API ? (
168+
<ChatGPTAPISettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
169+
) : userConfig.chatgptMode === ChatGPTMode.Azure ? (
170+
<ChatGPTAzureSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
171+
) : userConfig.chatgptMode === ChatGPTMode.Poe ? (
172+
<ChatGPTPoeSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
173+
) : userConfig.chatgptMode === ChatGPTMode.OpenRouter ? (
174+
<ChatGPTOpenRouterSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
175+
) : (
176+
<ChatGPWebSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
177+
)}
178+
</ChatBotSettingPanel>
179+
<ChatBotSettingPanel title="Claude">
180+
<RadioGroup
181+
options={Object.entries(ClaudeMode).map(([k, v]) => ({ label: `${k} ${t('Mode')}`, value: v }))}
182+
value={userConfig.claudeMode}
183+
onChange={(v) => updateConfigValue({ claudeMode: v as ClaudeMode })}
184+
/>
185+
{userConfig.claudeMode === ClaudeMode.API ? (
186+
<ClaudeAPISettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
187+
) : userConfig.claudeMode === ClaudeMode.Webapp ? (
188+
<ClaudeWebappSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
189+
) : userConfig.claudeMode === ClaudeMode.OpenRouter ? (
190+
<ClaudeOpenRouterSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
191+
) : (
192+
<ClaudePoeSettings userConfig={userConfig} updateConfigValue={updateConfigValue} />
193+
)}
194+
</ChatBotSettingPanel>
195+
<ChatBotSettingPanel title="Bing">
196+
<div className="flex flex-row gap-3 items-center justify-between w-[250px]">
197+
<p className="font-medium text-base">{t('Chat style')}</p>
198+
<div className="w-[150px]">
199+
<Select
200+
options={BING_STYLE_OPTIONS}
201+
value={userConfig.bingConversationStyle}
202+
onChange={(v) => updateConfigValue({ bingConversationStyle: v })}
203+
position="top"
204+
/>
205+
</div>
206+
</div>
207+
</ChatBotSettingPanel>
208+
</div>
209+
<div>
210+
{dirty && (
211+
<MotionButton
212+
color="primary"
213+
text={t('Save')}
214+
className="w-fit fixed bottom-10"
215+
onClick={save}
216+
animate={{ opacity: [0, 1] }}
217+
transition={{ duration: 0.3 }}
218+
/>
219+
)}
220+
</div>
221+
<Toaster position="top-right" />
204222
</div>
205-
<Button color={dirty ? 'primary' : 'flat'} text={t('Save')} className="w-fit my-8" onClick={save} />
206-
<Toaster position="top-right" />
207223
</PagePanel>
208224
)
209225
}

0 commit comments

Comments
 (0)