Skip to content

Commit 9a278b3

Browse files
committed
Merge branch 'main' into al/conditional-header-and-footer
2 parents 5c38c2e + 072c4d6 commit 9a278b3

File tree

19 files changed

+705
-85
lines changed

19 files changed

+705
-85
lines changed

backend/src/controllers/tools.ts

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Request, Response } from 'express'
22
import { PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
33
import _ from 'underscore'
44
import {
5+
filterDeepProperties,
56
getDefaultData,
67
getS3AndParams,
78
streamToString
@@ -19,19 +20,22 @@ export const getDefault = async (_: Request, res: Response) => {
1920
}
2021
}
2122

22-
export const saveUserConfig = async (req: Request, res: Response) => {
23+
export const createUserConfig = async (req: Request, res: Response) => {
2324
try {
2425
const data = req.body
26+
const tag = data.version || data.tag
2527

2628
if (!data.walletAddress) {
2729
throw 'Wallet address is required'
2830
}
31+
const defaultData = await getDefaultData()
32+
const defaultDataContent = JSON.parse(defaultData).default
33+
defaultDataContent.walletAddress = decodeURIComponent(
34+
`https://${data.walletAddress}`
35+
)
2936

3037
const { s3, params } = getS3AndParams(data.walletAddress)
3138

32-
// get defaults, get existing config, then overwrite values
33-
const defaultData = await getDefaultData()
34-
3539
let fileContentString = '{}'
3640
try {
3741
// existing config
@@ -51,23 +55,53 @@ export const saveUserConfig = async (req: Request, res: Response) => {
5155
}
5256
}
5357

54-
const changedValues = _.omit(data, function (value, key) {
55-
return defaultData[key] === value
56-
})
58+
let currentData = JSON.parse(fileContentString)
5759

58-
const currentData = Object.assign(
59-
JSON.parse(defaultData),
60-
JSON.parse(fileContentString)
61-
)
62-
const fileContent = JSON.stringify(
63-
Object.assign(currentData, ...[changedValues])
64-
)
60+
if (currentData?.default) {
61+
currentData = Object.assign(filterDeepProperties(currentData), {
62+
[tag]: defaultDataContent
63+
})
64+
} else {
65+
currentData = Object.assign(
66+
{ default: currentData },
67+
{
68+
[tag]: defaultDataContent
69+
}
70+
)
71+
}
72+
73+
const fileContent = JSON.stringify(currentData)
74+
const extendedParams = { ...params, Body: fileContent }
75+
76+
// save json to file
77+
await s3.send(new PutObjectCommand(extendedParams))
78+
79+
res.status(200).send(currentData)
80+
} catch (error) {
81+
console.log(error)
82+
res.status(500).send('An error occurred when fetching data')
83+
}
84+
}
85+
86+
export const saveUserConfig = async (req: Request, res: Response) => {
87+
try {
88+
const data = req.body
89+
90+
if (!data.walletAddress) {
91+
throw 'Wallet address is required'
92+
}
93+
94+
const { s3, params } = getS3AndParams(data.walletAddress)
6595

96+
// filter data so we are saving only config and none of the extra params received
97+
const fullConfig = JSON.parse(data?.fullconfig)
98+
const filteredData = filterDeepProperties(fullConfig)
99+
const fileContent = JSON.stringify(filteredData)
66100
const extendedParams = { ...params, Body: fileContent }
67101

68102
await s3.send(new PutObjectCommand(extendedParams))
69103

70-
res.status(200).send(data)
104+
res.status(200).send(filteredData)
71105
} catch (err) {
72106
console.log(err)
73107
res.status(500).send('An error occurred when saving data')
@@ -92,10 +126,49 @@ export const getUserConfig = async (req: Request, res: Response) => {
92126
data.Body as NodeJS.ReadableStream
93127
)
94128

95-
const fileContent = Object.assign(
129+
let fileContent = Object.assign(
96130
JSON.parse(defaultData),
97131
...[JSON.parse(fileContentString)]
98132
)
133+
fileContent = filterDeepProperties(fileContent)
134+
135+
res.status(200).send(fileContent)
136+
} catch (error) {
137+
const err = error as Error
138+
if (err.name === 'NoSuchKey') {
139+
// file / config not found, serve default
140+
const defaultData = await getDefaultData()
141+
res.status(200).send(defaultData)
142+
} else {
143+
console.log(error)
144+
res.status(500).send('An error occurred while fetching data')
145+
}
146+
}
147+
}
148+
149+
export const getUserConfigByTag = async (req: Request, res: Response) => {
150+
try {
151+
const id = req.params.id
152+
const tag = req.params.tag ?? 'default'
153+
154+
if (!id) {
155+
throw new S3FileNotFoundError('Wallet address is required')
156+
}
157+
158+
// ensure we have all keys w default values, user config will overwrite values that exist in saved json
159+
const defaultDataResp = await getDefaultData()
160+
const defaultData = JSON.parse(defaultDataResp)?.default
161+
162+
const { s3, params } = getS3AndParams(id)
163+
const data = await s3.send(new GetObjectCommand(params))
164+
// Convert the file stream to a string
165+
const fileContentString = await streamToString(
166+
data.Body as NodeJS.ReadableStream
167+
)
168+
169+
const userConfig = JSON.parse(fileContentString)
170+
const selectedConfig = userConfig[tag] ?? defaultData
171+
const fileContent = Object.assign(defaultData, ...[selectedConfig])
99172

100173
res.status(200).send(fileContent)
101174
} catch (error) {
@@ -114,5 +187,6 @@ export const getUserConfig = async (req: Request, res: Response) => {
114187
export default {
115188
getDefault,
116189
getUserConfig,
190+
createUserConfig,
117191
saveUserConfig
118192
}
Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
11
{
2-
"buttonFontName": "Arial",
3-
"buttonText": "Support me",
4-
"buttonBorder": "Light",
5-
"buttonTextColor": "#ffffff",
6-
"buttonBackgroundColor": "#ff808c",
7-
"bannerFontName": "Arial",
8-
"bannerFontSize": 16,
9-
"bannerTitleText": "How to support?",
10-
"bannerDescriptionText": "You can support this page and my work by a one time donation or proportional to the time you spend on this website through web monetization.",
11-
"bannerSlideAnimation": "Down",
12-
"bannerPosition": "Bottom",
13-
"bannerTextColor": "#ffffff",
14-
"bannerBackgroundColor": "#7f76b2",
15-
"bannerBorder": "Light",
16-
"widgetFontName": "Arial",
17-
"widgetFontSize": 16,
18-
"widgetDonateAmount": 1,
19-
"widgetTitleText": "Future of support",
20-
"widgetDescriptionText": "Experience the new way to support our content. Activate Web Monetization in your browser and support our work as you browse. Every visit helps us keep creating the content you love! You can also support us by a one time donation below!",
21-
"widgetButtonText": "Support me",
22-
"widgetButtonBackgroundColor": "#4ec6c0",
23-
"widgetButtonTextColor": "#000000",
24-
"widgetButtonBorder": "Light",
25-
"widgetTextColor": "#000000",
26-
"widgetBackgroundColor": "#ffffff",
27-
"widgetTriggerBackgroundColor": "#ffffff",
28-
"widgetTriggerIcon": "",
29-
"css": "H4sIAAAAAAAAA61S227bMAz9lW5-WYFQCFIkA2ygQL8koCy64UZJrkR1Ngz_-2BtaRds6NP4JJE6Fx7IsIw4n2108zLEoDCgZ5nbp8QouzxnJQ-FdxlDhkyJh0_sx5gUg3Z9lJjaZqjVread684W1RjMD68wxORRFa0Q_Govt0CL_ffnFEtw8HuAB-uc7WxMjhIkdFxyuzcPX4-JfKcJQ2blGFoU2ZtjJsx0q2_QxxIUHOdRcN79ORPeNTQphcwxwIhztXgnaEmWD3ZqLoROKOfCUCMQSDHqgiJtoldK2q3_IuYwFr0S72t98NA4zltWbvk7F6p1zf10OnVr81KiUsU_On41g9DUBr1Af2FxXx7ul60DjhP1NbI-SvGhW5vzOdCkbyBzgaGI1POmZ1jJZ-gpKCXzrWTlYb5exwmOG_Lz_eby1uQ1uv8iUNFe4bA3SpOCxUyLx_TMATSO7WH7EOtPZC6FXcYCAAA="
2+
"default": {
3+
"buttonFontName": "Arial",
4+
"buttonText": "Support me",
5+
"buttonBorder": "Light",
6+
"buttonTextColor": "#ffffff",
7+
"buttonBackgroundColor": "#ff808c",
8+
"bannerFontName": "Arial",
9+
"bannerFontSize": 16,
10+
"bannerTitleText": "How to support?",
11+
"bannerDescriptionText": "You can support this page and my work by a one time donation or proportional to the time you spend on this website through web monetization.",
12+
"bannerSlideAnimation": "Down",
13+
"bannerPosition": "Bottom",
14+
"bannerTextColor": "#ffffff",
15+
"bannerBackgroundColor": "#7f76b2",
16+
"bannerBorder": "Light",
17+
"widgetFontName": "Arial",
18+
"widgetFontSize": 16,
19+
"widgetDonateAmount": 1,
20+
"widgetTitleText": "Future of support",
21+
"widgetDescriptionText": "Experience the new way to support our content. Activate Web Monetization in your browser and support our work as you browse. Every visit helps us keep creating the content you love! You can also support us by a one time donation below!",
22+
"widgetButtonText": "Support me",
23+
"widgetButtonBackgroundColor": "#4ec6c0",
24+
"widgetButtonTextColor": "#000000",
25+
"widgetButtonBorder": "Light",
26+
"widgetTextColor": "#000000",
27+
"widgetBackgroundColor": "#ffffff",
28+
"widgetTriggerBackgroundColor": "#ffffff",
29+
"widgetTriggerIcon": "",
30+
"css": "H4sIAAAAAAAAA61S227bMAz9lW5-WYFQCFIkA2ygQL8koCy64UZJrkR1Ngz_-2BtaRds6NP4JJE6Fx7IsIw4n2108zLEoDCgZ5nbp8QouzxnJQ-FdxlDhkyJh0_sx5gUg3Z9lJjaZqjVread684W1RjMD68wxORRFa0Q_Govt0CL_ffnFEtw8HuAB-uc7WxMjhIkdFxyuzcPX4-JfKcJQ2blGFoU2ZtjJsx0q2_QxxIUHOdRcN79ORPeNTQphcwxwIhztXgnaEmWD3ZqLoROKOfCUCMQSDHqgiJtoldK2q3_IuYwFr0S72t98NA4zltWbvk7F6p1zf10OnVr81KiUsU_On41g9DUBr1Af2FxXx7ul60DjhP1NbI-SvGhW5vzOdCkbyBzgaGI1POmZ1jJZ-gpKCXzrWTlYb5exwmOG_Lz_eby1uQ1uv8iUNFe4bA3SpOCxUyLx_TMATSO7WH7EOtPZC6FXcYCAAA="
31+
}
3032
}

backend/src/routes/tools.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import type { Router } from 'express'
22
import {
33
getDefault,
44
getUserConfig,
5-
saveUserConfig
5+
createUserConfig,
6+
saveUserConfig,
7+
getUserConfigByTag
68
} from '../controllers/tools.js'
79

810
const userRoutes = (router: Router) => {
911
router.get('/tools/default', getDefault)
1012
router.get('/tools/:id', getUserConfig)
11-
router.post('/tools', saveUserConfig)
13+
router.get('/tools/:id/:tag', getUserConfigByTag)
14+
router.post('/tools', createUserConfig)
15+
router.put('/tools', saveUserConfig)
1216

1317
return router
1418
}

backend/src/services/utils.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const getS3AndParams = (walletAddress: string) => {
2121
})
2222
})
2323

24-
const fileKey = `${walletAddress
24+
const fileKey = `${decodeURIComponent(walletAddress)
2525
.replace('$', '')
2626
.replace('https://', '')}.json`
2727

@@ -54,3 +54,44 @@ export const streamToString = (
5454
readableStream.on('error', reject)
5555
})
5656
}
57+
58+
// return only properties that are at least levelCount deep
59+
/* eslint-disable @typescript-eslint/no-explicit-any */
60+
export const filterDeepProperties = (
61+
obj: Record<string, any>,
62+
levelCount: number = 2
63+
): Record<string, any> => {
64+
const result: Record<string, any> = {}
65+
66+
const traverse = (
67+
current: any,
68+
path: string[],
69+
parent: Record<string, any>
70+
) => {
71+
if (typeof current === 'object' && current !== null) {
72+
for (const key in current) {
73+
if (Object.prototype.hasOwnProperty.call(current, key)) {
74+
const newPath = [...path, key]
75+
76+
if (typeof current[key] === 'object' && current[key] !== null) {
77+
// Ensure parent structure exists
78+
if (path.length === 0) {
79+
if (!result[key]) result[key] = {}
80+
traverse(current[key], newPath, result[key])
81+
} else {
82+
if (!parent[key]) parent[key] = {}
83+
traverse(current[key], newPath, parent[key])
84+
}
85+
} else if (path.length >= levelCount - 1) {
86+
// Only keep properties that are at least levelCount levels deep
87+
if (!result[path[0]]) result[path[0]] = {}
88+
result[path[0]][key] = current[key]
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
traverse(obj, [], result)
96+
return result
97+
}

eslint.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ export default [
3939
mjs: 'always',
4040
jsx: 'always'
4141
}
42+
],
43+
'@typescript-eslint/no-unused-vars': [
44+
'warn',
45+
{
46+
argsIgnorePattern: '^_',
47+
varsIgnorePattern: '^_',
48+
caughtErrorsIgnorePattern: '^_'
49+
}
4250
]
4351
}
4452
},

frontend/app/components/Button.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const buttonStyles = cva(
1313
'text-sm hover:from-wm-purple hover:to-[#7f7fff] hover:text-white hover:bg-gradient-to-r',
1414
danger:
1515
'disabled:bg-red-200 bg-red-500 hover:bg-red-600 shadow-md text-white',
16+
icon: 'enabled:hover:bg-gradient-to-r enabled:hover:from-wm-green enabled:hover:to-wm-green-fade aspect-square shadow-md max-w-[30px] disabled:bg-gray-200 disabled:hover:bg-gray-200!',
1617
invisible: 'px-1 border-none text-white'
1718
},
1819
size: {

frontend/app/components/PageHeader.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
import { useNavigate } from '@remix-run/react'
22
import { cx } from 'class-variance-authority'
33
import { availableTools } from '~/lib/presets.js'
4-
import { Button, InfoWithTooltip } from './index.js'
4+
import { Button, InfoWithTooltip, Select, SelectOption } from './index.js'
55
import { Chevron } from './icons.js'
66

77
export const PageHeader = ({
88
elementType,
99
title,
1010
link,
11-
setImportModalOpen
11+
setImportModalOpen,
12+
setNewVersionModalOpen,
13+
setConfirmModalOpen,
14+
versionOptions,
15+
selectedVersion,
16+
setSelectedVersion
1217
}: {
1318
elementType: string | undefined
1419
title: string
1520
link: string
1621
setImportModalOpen: React.Dispatch<React.SetStateAction<boolean>>
22+
setNewVersionModalOpen: React.Dispatch<React.SetStateAction<boolean>>
23+
setConfirmModalOpen: React.Dispatch<React.SetStateAction<boolean>>
24+
versionOptions: SelectOption[]
25+
selectedVersion: string
26+
setSelectedVersion: (value: string) => void
1727
}) => {
1828
const navigate = useNavigate()
1929

@@ -33,6 +43,37 @@ export const PageHeader = ({
3343
<InfoWithTooltip tooltip={currentElement?.tooltip} />
3444
</h3>
3545
</div>
46+
<div className="flex mr-2">
47+
<Select
48+
placeholder="Default"
49+
options={versionOptions}
50+
value={versionOptions.find((opt) => opt.value == selectedVersion)}
51+
onChange={(value) => setSelectedVersion(value)}
52+
/>
53+
<Button
54+
intent="icon"
55+
className="mr-2 pt-0"
56+
aria-label="add version"
57+
title="add version"
58+
onClick={() => {
59+
setNewVersionModalOpen(true)
60+
}}
61+
>
62+
+
63+
</Button>
64+
<Button
65+
intent="icon"
66+
className="mr-2 pt-0 text-red-500 "
67+
aria-label="remove version"
68+
title="remove version"
69+
disabled={selectedVersion == 'default'}
70+
onClick={() => {
71+
setConfirmModalOpen(true)
72+
}}
73+
>
74+
x
75+
</Button>
76+
</div>
3677
<div className="ml-auto">
3778
<Button
3879
className="mr-2"

frontend/app/components/WalletAddressInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const WalletAddress = ({
1717
name="walletAddress"
1818
label="Wallet address"
1919
tooltip={tooltips.walletAddress}
20-
value={config.walletAddress || ''}
20+
value={config?.walletAddress || ''}
2121
placeholder="https://ase-provider-url/jdoe"
2222
error={errors?.fieldErrors.walletAddress}
2323
onChange={(e) =>

0 commit comments

Comments
 (0)