index.tsx164 lines · main
1'use client'
2
3import { useConsentState } from 'common'
4import Link from 'next/link'
5import { PropsWithChildren, useState } from 'react'
6import {
7 Button,
8 Dialog,
9 DialogContent,
10 DialogFooter,
11 DialogHeader,
12 DialogSection,
13 DialogSectionSeparator,
14 DialogTitle,
15 DialogTrigger,
16 Label,
17 Switch,
18} from 'ui'
19
20import { Admonition } from '../admonition'
21
22interface PrivacySettingsProps {
23 className?: string
24}
25
26export const PrivacySettings = ({
27 children,
28 ...props
29}: PropsWithChildren<PrivacySettingsProps>) => {
30 const [isOpen, setIsOpen] = useState(false)
31 const { categories, updateServices } = useConsentState()
32
33 const [serviceConsentMap, setServiceConsentMap] = useState(() => new Map<string, boolean>())
34
35 function handleServicesChange(services: { id: string; status: boolean }[]) {
36 let newServiceConsentMap = new Map(serviceConsentMap)
37 services.forEach((service) => {
38 newServiceConsentMap.set(service.id, service.status)
39 })
40 setServiceConsentMap(newServiceConsentMap)
41 }
42
43 const handleConfirmPreferences = () => {
44 const services = Array.from(serviceConsentMap.entries()).map(([id, status]) => ({
45 serviceId: id,
46 status,
47 }))
48 updateServices(services)
49
50 setIsOpen(false)
51 }
52
53 return (
54 <Dialog open={isOpen} onOpenChange={(open) => setIsOpen(open)}>
55 <DialogTrigger asChild>
56 <button {...props} onClick={() => setIsOpen(true)}>
57 {children}
58 </button>
59 </DialogTrigger>
60 <DialogContent size="medium">
61 <DialogHeader>
62 <DialogTitle>Privacy Settings</DialogTitle>
63 </DialogHeader>
64 <DialogSectionSeparator />
65 <DialogSection className="space-y-4">
66 <div className="divide-y divide-border">
67 {categories === null ? (
68 <Admonition
69 type="warning"
70 title="Unable to Load Privacy Settings"
71 description={
72 <>
73 We couldn't load the privacy settings due to an ad blocker or network error.
74 Please disable any ad blockers and try again. If the problem persists, please{' '}
75 <Link href="https://supabase.com/dashboard/support/new" className="underline">
76 contact support
77 </Link>
78 .
79 </>
80 }
81 />
82 ) : (
83 [...categories]
84 .reverse()
85 .map((category) => (
86 <Category
87 key={category.slug}
88 category={category}
89 handleServicesChange={handleServicesChange}
90 />
91 ))
92 )}
93 </div>
94 </DialogSection>
95 <DialogFooter>
96 <Button onClick={() => setIsOpen(false)}>Cancel</Button>
97 <Button onClick={handleConfirmPreferences}>Confirm</Button>
98 </DialogFooter>
99 </DialogContent>
100 </Dialog>
101 )
102}
103
104function Category({
105 category,
106 handleServicesChange,
107}: {
108 category: {
109 slug: string
110 label: string
111 description: string
112 isEssential: boolean
113 services: readonly {
114 id: string
115 consent: {
116 status: boolean
117 }
118 }[]
119 }
120 handleServicesChange: (services: { id: string; status: boolean }[]) => void
121}) {
122 const [isChecked, setIsChecked] = useState(() =>
123 category.services.every((service) => service.consent.status)
124 )
125
126 function handleChange() {
127 setIsChecked(!isChecked)
128
129 handleServicesChange(
130 category.services.map((service) => ({
131 id: service.id,
132 status: !isChecked,
133 }))
134 )
135 }
136
137 return (
138 <div className="flex flex-row items-center justify-between gap-4" key={category.slug}>
139 <div className="space-y-0.5">
140 <Label className="text-base" htmlFor={category.slug}>
141 {category.label}
142 </Label>
143 <div className="text-sm text-foreground-light" id={`${category.slug}-description`}>
144 {category.description}
145 <br />
146 <Link
147 href="https://supabase.com/privacy#8-cookies-and-similar-technologies-used-on-our-european-services"
148 className="underline"
149 >
150 Learn more
151 </Link>
152 </div>
153 </div>
154 <Switch
155 id={category.slug}
156 checked={isChecked}
157 disabled={category.isEssential}
158 defaultChecked={isChecked}
159 onCheckedChange={handleChange}
160 aria-describedby={`${category.slug}-description`}
161 />
162 </div>
163 )
164}