Skip to content

Commit 45f9926

Browse files
committed
wip: upsert sub branding, territory branding form, apply styles
- get branding with ssrApollo - apply styles via DomainProvider - ColorPicker, BrandingUpload components - territory branding validation schema - fragments, resolvers, typedefs -- WIP
1 parent 127cfca commit 45f9926

File tree

7 files changed

+203
-9
lines changed

7 files changed

+203
-9
lines changed

api/resolvers/sub.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { whenRange } from '@/lib/time'
2-
import { validateSchema, territorySchema } from '@/lib/validate'
2+
import { validateSchema, territorySchema, subBrandingSchema } from '@/lib/validate'
33
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
44
import { viewGroup } from './growth'
55
import { notifyTerritoryTransfer } from '@/lib/webPush'
@@ -170,6 +170,9 @@ export default {
170170
cursor: subs.length === limit ? nextCursorEncoded(decodedCursor, limit) : null,
171171
subs
172172
}
173+
},
174+
subBranding: async (parent, { subName }, { models }) => {
175+
return models.subBranding.findUnique({ where: { subName } })
173176
}
174177
},
175178
Mutation: {
@@ -298,6 +301,28 @@ export default {
298301
}
299302

300303
return await performPaidAction('TERRITORY_UNARCHIVE', data, { me, models, lnd })
304+
},
305+
upsertSubBranding: async (parent, { subName, branding }, { me, models }) => {
306+
if (!me) {
307+
throw new GqlAuthenticationError()
308+
}
309+
310+
const sub = await models.sub.findUnique({ where: { name: subName } })
311+
if (!sub) {
312+
throw new GqlInputError('sub not found')
313+
}
314+
315+
if (sub.userId !== me.id) {
316+
throw new GqlInputError('you do not own this sub')
317+
}
318+
319+
await validateSchema(subBrandingSchema, branding)
320+
321+
return await models.subBranding.upsert({
322+
where: { subName },
323+
update: { ...branding, subName },
324+
create: { ...branding, subName }
325+
})
301326
}
302327
},
303328
Sub: {

api/ssrApollo.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getAuthOptions } from '@/pages/api/auth/[...nextauth]'
1616
import { NOFOLLOW_LIMIT } from '@/lib/constants'
1717
import { satsToMsats } from '@/lib/format'
1818
import { MULTI_AUTH_ANON, MULTI_AUTH_LIST } from '@/lib/auth'
19+
import { SUB_BRANDING } from '@/fragments/subs'
1920

2021
export default async function getSSRApolloClient ({ req, res, me = null }) {
2122
const session = req && await getServerSession(req, res, getAuthOptions(req))
@@ -156,10 +157,15 @@ export function getGetServerSideProps (
156157
const subName = req.headers['x-stacker-news-subname'] || null
157158
let domain = null
158159
if (isCustomDomain && subName) {
160+
const { data: { subBranding } } = await client.query({
161+
query: SUB_BRANDING,
162+
variables: { subName }
163+
})
164+
console.log('subBranding', subBranding)
159165
domain = {
160166
domainName: req.headers.host,
161-
subName
162-
// TODO: custom branding
167+
subName,
168+
branding: subBranding || null
163169
}
164170
}
165171

api/typeDefs/sub.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default gql`
3030
replyCost: Int!, postTypes: [String!]!,
3131
billingType: String!, billingAutoRenew: Boolean!,
3232
moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
33-
setSubBranding(subName: String!, branding: SubBrandingInput): SubBranding!
33+
upsertSubBranding(subName: String!, branding: SubBrandingInput): SubBranding!
3434
}
3535
3636
type Sub {
@@ -59,6 +59,7 @@ export default gql`
5959
ncomments(when: String, from: String, to: String): Int!
6060
meSubscription: Boolean!
6161
domain: Domain
62+
branding: SubBranding
6263
optional: SubOptional!
6364
}
6465

components/form.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import textAreaCaret from 'textarea-caret'
2424
import 'react-datepicker/dist/react-datepicker.css'
2525
import useDebounceCallback, { debounce } from './use-debounce-callback'
2626
import { FileUpload } from './file-upload'
27-
import { AWS_S3_URL_REGEXP } from '@/lib/constants'
27+
import { AWS_S3_URL_REGEXP, MEDIA_URL } from '@/lib/constants'
2828
import { whenRange } from '@/lib/time'
2929
import { useFeeButton } from './fee-button'
3030
import Thumb from '@/svgs/thumb-up-fill.svg'
@@ -42,6 +42,7 @@ import dynamic from 'next/dynamic'
4242
import { qrImageSettings } from './qr'
4343
import { useIsClient } from './use-client'
4444
import PageLoading from './page-loading'
45+
import Avatar from './avatar'
4546

4647
export class SessionRequiredError extends Error {
4748
constructor () {
@@ -1532,5 +1533,54 @@ export function MultiInput ({
15321533
)
15331534
}
15341535

1536+
export function ColorPicker ({ label, groupClassName, name, ...props }) {
1537+
const [field, , helpers] = useField({ ...props, name })
1538+
1539+
useEffect(() => {
1540+
helpers.setValue(field.value)
1541+
}, [field.value])
1542+
1543+
return (
1544+
<FormGroup label={label} className={groupClassName}>
1545+
<ClientInput
1546+
type='color'
1547+
{...field}
1548+
{...props}
1549+
onChange={(formik, e) => {
1550+
field.onChange(e)
1551+
if (props.onChange) {
1552+
props.onChange(formik, e)
1553+
}
1554+
}}
1555+
/>
1556+
</FormGroup>
1557+
)
1558+
}
1559+
1560+
export function BrandingUpload ({ label, groupClassName, name, ...props }) {
1561+
const [field, , helpers] = useField({ ...props, name })
1562+
const [tempId, setTempId] = useState(field.value)
1563+
1564+
const handleSuccess = useCallback((id) => {
1565+
setTempId(id)
1566+
helpers.setValue(id)
1567+
}, [helpers])
1568+
1569+
return (
1570+
<FormGroup label={label} className={groupClassName}>
1571+
<img
1572+
src={tempId ? `${MEDIA_URL}/${tempId}` : '/favicon.png'}
1573+
alt={name}
1574+
width={100}
1575+
height={100}
1576+
style={{ objectFit: 'contain', position: 'relative' }}
1577+
/>
1578+
<Avatar
1579+
onSuccess={handleSuccess}
1580+
/>
1581+
</FormGroup>
1582+
)
1583+
}
1584+
15351585
export const ClientInput = Client(Input)
15361586
export const ClientCheckbox = Client(Checkbox)

components/territory-branding.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useToast } from '@/components/toast'
2+
import { SUB_BRANDING, UPSERT_SUB_BRANDING } from '@/fragments/subs'
3+
import { useMutation, useQuery } from '@apollo/client'
4+
import { subBrandingSchema } from '@/lib/validate'
5+
import { Form, Input, ColorPicker, SubmitButton } from '@/components/form'
6+
import AccordianItem from '@/components/accordian-item'
7+
8+
export default function TerritoryBrandingForm ({ sub }) {
9+
const [upsertSubBranding] = useMutation(UPSERT_SUB_BRANDING)
10+
const toaster = useToast()
11+
12+
const { data } = useQuery(SUB_BRANDING, {
13+
variables: { subName: sub.name }
14+
})
15+
16+
const subBranding = data?.sub?.branding
17+
18+
const onSubmit = async (values) => {
19+
try {
20+
await upsertSubBranding({
21+
variables: {
22+
subName: sub.name,
23+
branding: {
24+
title: values.title,
25+
description: values.description,
26+
primaryColor: values.primary,
27+
secondaryColor: values.secondary,
28+
logoId: values.logoId,
29+
faviconId: values.faviconId
30+
}
31+
}
32+
})
33+
toaster.success('Branding updated successfully')
34+
} catch (error) {
35+
console.error(error)
36+
toaster.danger('Failed to update branding', { error })
37+
}
38+
}
39+
40+
const initialValues = {
41+
title: subBranding?.title || sub?.name,
42+
description: subBranding?.description || '',
43+
primaryColor: subBranding?.primaryColor || '#FADA5E',
44+
secondaryColor: subBranding?.secondaryColor || '#F6911D',
45+
logoId: subBranding?.logoId || null,
46+
faviconId: subBranding?.faviconId || null
47+
}
48+
49+
return (
50+
<Form
51+
initial={initialValues}
52+
schema={subBrandingSchema}
53+
onSubmit={onSubmit}
54+
>
55+
<Input label='title' name='title' />
56+
<Input label='description' name='description' />
57+
<div className='row'>
58+
<ColorPicker groupClassName='col-4' label='primary color' name='primary' />
59+
<ColorPicker groupClassName='col-4' label='secondary color' name='secondary' />
60+
</div>
61+
<AccordianItem
62+
header={<div className='fw-bold text-muted'>logo and favicon</div>}
63+
body={
64+
<div className='row'>
65+
<div className='col-2'>
66+
<label className='form-label'>logo</label>
67+
<div style={{ position: 'relative', width: '100px', height: '100px', border: '1px solid #dee2e6', borderRadius: '5px', overflow: 'hidden' }}>
68+
<p>placeholder</p>
69+
{/* <BrandingUpload name='logoId' /> */}
70+
</div>
71+
</div>
72+
<div className='col-2'>
73+
<label className='form-label'>favicon</label>
74+
<div style={{ position: 'relative', width: '100px', height: '100px', border: '1px solid #dee2e6', borderRadius: '5px', overflow: 'hidden' }}>
75+
<p>placeholder</p>
76+
{/* <BrandingUpload name='faviconId' /> */}
77+
</div>
78+
</div>
79+
</div>
80+
}
81+
/>
82+
<div className='mt-3 d-flex justify-content-end'>
83+
<SubmitButton variant='primary'>save branding</SubmitButton>
84+
</div>
85+
</Form>
86+
)
87+
}

components/territory-domains.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Moon from '@/svgs/moon-fill.svg'
1010
import ClipboardLine from '@/svgs/clipboard-line.svg'
1111
import RefreshLine from '@/svgs/refresh-line.svg'
1212
import styles from './item.module.css'
13+
import TerritoryBrandingForm from './territory-branding'
1314

1415
// Domain context for custom domains
1516
const DomainContext = createContext({
@@ -25,12 +26,17 @@ export const DomainProvider = ({ domain: ssrDomain, children }) => {
2526

2627
// wip-brandings: this is a sketch/hack to set the branding colors
2728
const setBrandingColors = (branding) => {
28-
document.documentElement.style.setProperty('--bs-transition', 'all 0.3s ease')
29+
document.documentElement.style.setProperty('--bs-transition', 'all 1s ease')
2930
if (branding.primaryColor) {
30-
document.documentElement.style.setProperty('--theme-primary', branding.primaryColor)
31+
document.documentElement.style.setProperty('--bs-primary', branding.primaryColor)
32+
// wip-brandings: set the primary color to the navbar-brand svg
33+
const navbarBrand = document.querySelector('.navbar-brand')
34+
if (navbarBrand) {
35+
navbarBrand.style.fill = branding.primaryColor
36+
}
3137
}
3238
if (branding.secondaryColor) {
33-
document.documentElement.style.setProperty('--theme-secondary', branding.secondaryColor)
39+
document.documentElement.style.setProperty('--bs-secondary', branding.secondaryColor)
3440
}
3541

3642
return () => {
@@ -222,6 +228,7 @@ export default function CustomDomainForm ({ sub }) {
222228
{data?.domain && data?.domain?.status !== 'ACTIVE' && (
223229
<DomainGuidelines domain={data?.domain} />
224230
)}
231+
{data?.domain?.status === 'ACTIVE' && <TerritoryBrandingForm sub={sub} />}
225232
</>
226233
)
227234
}

fragments/subs.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export const TOP_SUBS = gql`
155155
}
156156
`
157157

158-
export const SUB_BRANDING = gql`
158+
export const SUB_BRANDING_FIELDS = gql`
159159
fragment SubBrandingFields on SubBranding {
160160
title
161161
description
@@ -165,3 +165,21 @@ export const SUB_BRANDING = gql`
165165
secondaryColor
166166
}
167167
`
168+
169+
export const SUB_BRANDING = gql`
170+
${SUB_BRANDING_FIELDS}
171+
query SubBranding($subName: String!) {
172+
subBranding(subName: $subName) {
173+
...SubBrandingFields
174+
}
175+
}
176+
`
177+
178+
export const UPSERT_SUB_BRANDING = gql`
179+
${SUB_BRANDING_FIELDS}
180+
mutation upsertSubBranding($subName: String!, $branding: SubBrandingInput) {
181+
upsertSubBranding(subName: $subName, branding: $branding) {
182+
...SubBrandingFields
183+
}
184+
}
185+
`

0 commit comments

Comments
 (0)