admin_lsac 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +46 -0
  2. package/build/asset-manifest.json +15 -0
  3. package/build/favicon.ico +0 -0
  4. package/build/index.html +1 -0
  5. package/build/logo192.png +0 -0
  6. package/build/logo512.png +0 -0
  7. package/build/manifest.json +25 -0
  8. package/build/robots.txt +3 -0
  9. package/build/static/css/main.6471d273.css +2 -0
  10. package/build/static/css/main.6471d273.css.map +1 -0
  11. package/build/static/js/592.91aa6b4b.chunk.js +2 -0
  12. package/build/static/js/592.91aa6b4b.chunk.js.map +1 -0
  13. package/build/static/js/main.366f5b12.js +3 -0
  14. package/build/static/js/main.366f5b12.js.LICENSE.txt +139 -0
  15. package/build/static/js/main.366f5b12.js.map +1 -0
  16. package/package.json +71 -0
  17. package/public/favicon.ico +0 -0
  18. package/public/index.html +43 -0
  19. package/public/logo192.png +0 -0
  20. package/public/logo512.png +0 -0
  21. package/public/manifest.json +25 -0
  22. package/public/robots.txt +3 -0
  23. package/server/.env.example +3 -0
  24. package/server/deploy.js +29 -0
  25. package/server/package-lock.json +712 -0
  26. package/server/package.json +18 -0
  27. package/src/App.css +38 -0
  28. package/src/App.tsx +92 -0
  29. package/src/components/GenericForm.tsx +281 -0
  30. package/src/components/Page.tsx +207 -0
  31. package/src/index.css +13 -0
  32. package/src/index.tsx +20 -0
  33. package/src/logo.svg +1 -0
  34. package/src/pages/event.ts +69 -0
  35. package/src/react-app-env.d.ts +1 -0
  36. package/src/reportWebVitals.ts +15 -0
  37. package/src/schema/schema.config.ts +6 -0
  38. package/src/schema/schema.ts +68 -0
  39. package/src/setupTests.ts +5 -0
  40. package/tsconfig.esm.json +7 -0
  41. package/tsconfig.json +31 -0
  42. package/webpack.config.js +22 -0
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "server",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "deploy.js",
6
+ "scripts": {
7
+ "start": "node deploy.js"
8
+ },
9
+ "author": "",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "dotenv": "^16.0.1",
13
+ "express": "^4.18.1",
14
+ "fs": "^0.0.1-security",
15
+ "https": "^1.0.0",
16
+ "path": "^0.12.7"
17
+ }
18
+ }
package/src/App.css ADDED
@@ -0,0 +1,38 @@
1
+ .App {
2
+ text-align: center;
3
+ }
4
+
5
+ .App-logo {
6
+ height: 40vmin;
7
+ pointer-events: none;
8
+ }
9
+
10
+ @media (prefers-reduced-motion: no-preference) {
11
+ .App-logo {
12
+ animation: App-logo-spin infinite 20s linear;
13
+ }
14
+ }
15
+
16
+ .App-header {
17
+ background-color: #282c34;
18
+ min-height: 100vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ justify-content: center;
23
+ font-size: calc(10px + 2vmin);
24
+ color: white;
25
+ }
26
+
27
+ .App-link {
28
+ color: #61dafb;
29
+ }
30
+
31
+ @keyframes App-logo-spin {
32
+ from {
33
+ transform: rotate(0deg);
34
+ }
35
+ to {
36
+ transform: rotate(360deg);
37
+ }
38
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,92 @@
1
+ import { ChakraProvider } from "@chakra-ui/react";
2
+ import { BrowserRouter, Route, Routes } from "react-router-dom";
3
+ import { Page } from "./components/Page";
4
+ import { pageToUrl } from "./schema/schema";
5
+ import * as React from "react";
6
+
7
+ export interface TableField {
8
+ name: string;
9
+ access: (obj: any) => string | number | React.ReactElement;
10
+ }
11
+
12
+ export interface SimpleField {
13
+ name: string;
14
+ label: string;
15
+ type: "string" | "number" | "file";
16
+ validationSchema: any;
17
+ }
18
+
19
+ export interface Option {
20
+ value: string | number | boolean;
21
+ display: string;
22
+ }
23
+
24
+ export interface DropdownField {
25
+ name: string;
26
+ label: string;
27
+ type: "dropdown";
28
+ options?: Option[] | undefined;
29
+ getOptions: () => Promise<Option[]>;
30
+ }
31
+
32
+ export interface ObjectField {
33
+ name: string;
34
+ label: string;
35
+ type: "object";
36
+ fields: FormField[];
37
+ }
38
+
39
+ export interface ArrayField {
40
+ name: string;
41
+ label: string;
42
+ type: "array";
43
+ element: Exclude<FormField, ArrayField>;
44
+ }
45
+
46
+ export type FormField = SimpleField | DropdownField | ObjectField | ArrayField;
47
+
48
+ export interface Action {
49
+ name: string;
50
+ fields?: FormField[];
51
+ onSubmit: (obj1: any, obj2?: any) => Promise<any>;
52
+ }
53
+
54
+ export interface RowAction {
55
+ name: string;
56
+ fields?: FormField[];
57
+ initialise?: (obj: any) => any;
58
+ onSubmit: (obj1: any, obj2?: any) => Promise<any>;
59
+ }
60
+
61
+ export interface PageSchema {
62
+ name: string;
63
+ actions: Action[];
64
+ rowActions: RowAction[];
65
+ getRequest: () => Promise<any[]>;
66
+ tableFields: TableField[];
67
+ }
68
+
69
+ function AdminPanel({ pages }: { pages: PageSchema[] }) {
70
+ return (
71
+ <ChakraProvider>
72
+ <BrowserRouter>
73
+ <Routes>
74
+ {pages.map((page) => (
75
+ <Route
76
+ path={pageToUrl(page)}
77
+ element={<Page pages={pages} selectedPage={page} />}
78
+ />
79
+ ))}
80
+ {pages[0] ?
81
+ <Route
82
+ path="*"
83
+ element={<Page pages={pages} selectedPage={pages[0]} />}
84
+ />
85
+ : null}
86
+ </Routes>
87
+ </BrowserRouter>
88
+ </ChakraProvider>
89
+ );
90
+ }
91
+
92
+ export default AdminPanel;
@@ -0,0 +1,281 @@
1
+ import {
2
+ Box,
3
+ Button,
4
+ FormControl,
5
+ FormErrorMessage,
6
+ FormLabel,
7
+ Input,
8
+ Select,
9
+ } from "@chakra-ui/react";
10
+ import { Field, Form, Formik, FieldArray, getIn } from "formik";
11
+ import {
12
+ Action,
13
+ FormField,
14
+ SimpleField,
15
+ DropdownField,
16
+ RowAction,
17
+ } from "../schema/schema";
18
+ import * as Yup from "yup";
19
+ import Swal from "sweetalert2";
20
+ import { useEffect, useState } from "react";
21
+
22
+ interface GenericFormProps {
23
+ action: Action | RowAction;
24
+ onClose?: () => void;
25
+ rowId?: number;
26
+ }
27
+
28
+ function parseValidation(fields: FormField[] | undefined) {
29
+ if (!fields) {
30
+ return {};
31
+ }
32
+
33
+ return fields.reduce((acc: any, field) => {
34
+ if (
35
+ field.type !== "dropdown" &&
36
+ field.type !== "object" &&
37
+ field.type !== "array"
38
+ ) {
39
+ acc[field.name] = field.validationSchema;
40
+ } else if (field.type === "array") {
41
+ if (field.element.type === "dropdown") {
42
+ acc[field.name] = Yup.array().of(Yup.string().required());
43
+ } else if (field.element.type === "object") {
44
+ acc[field.name] = Yup.array().of(
45
+ Yup.object(parseValidation(field.element.fields))
46
+ );
47
+ } else {
48
+ acc[field.name] = Yup.array().of(field.element.validationSchema);
49
+ }
50
+ } else if (field.type === "object") {
51
+ acc[field.name] = Yup.object(parseValidation(field.fields));
52
+ }
53
+ if (field.type === "dropdown") {
54
+ acc[field.name] = Yup.string().required();
55
+ }
56
+ return acc;
57
+ }, {});
58
+ }
59
+
60
+ function generateSingleField(
61
+ formfield: SimpleField | DropdownField,
62
+ fieldName: string,
63
+ key: any
64
+ ) {
65
+ return (
66
+ <Field name={fieldName} key={key}>
67
+ {({ field, form }: any) => (
68
+ <FormControl
69
+ isInvalid={
70
+ getIn(form.touched, fieldName) && getIn(form.errors, fieldName)
71
+ }
72
+ // paddingLeft={40 * countIndent(fieldName)}
73
+ >
74
+ <FormLabel marginTop={2}>{formfield.label}</FormLabel>
75
+
76
+ {formfield.type === "string" || formfield.type === "number" ? (
77
+ <Input
78
+ {...field}
79
+ type={formfield.type === "number" ? "number" : "text"}
80
+ placeholder=""
81
+ />
82
+ ) : formfield.type === "file" ? (
83
+ <Input
84
+ type="file"
85
+ name={fieldName}
86
+ onChange={(event) => {
87
+ if (event.currentTarget.files != null) {
88
+ form.setFieldValue(fieldName, event.currentTarget.files[0]);
89
+ }
90
+ }}
91
+ />
92
+ ) : (
93
+ <Select {...field} defaultValue="">
94
+ <option disabled={true} value="">
95
+ Select an option!
96
+ </option>
97
+ {(formfield as DropdownField).options?.map((option, key) => (
98
+ <option key={key} value={`${option.value}`}>
99
+ {option.display}
100
+ </option>
101
+ ))}
102
+ </Select>
103
+ )}
104
+
105
+ <FormErrorMessage>{getIn(form.errors, fieldName)}</FormErrorMessage>
106
+ </FormControl>
107
+ )}
108
+ </Field>
109
+ );
110
+ }
111
+
112
+ function parseFields(
113
+ fields: FormField[] | undefined,
114
+ previous?: string | undefined
115
+ ) {
116
+ if (!fields) {
117
+ return [];
118
+ }
119
+
120
+ const computedFields = fields.map((formfield, key) => {
121
+ let fieldName: string =
122
+ previous !== undefined ? previous + "." + formfield.name : formfield.name;
123
+
124
+ if (formfield.type === "object") {
125
+ const objectFields = parseFields(formfield.fields, fieldName);
126
+ return <FormControl key={fieldName}>{objectFields}</FormControl>;
127
+ } else if (formfield.type === "array") {
128
+ return (
129
+ <FieldArray name={fieldName}>
130
+ {({ remove, push, form }) => (
131
+ <div>
132
+ <FormLabel marginTop={2}>{formfield.label}</FormLabel>
133
+ {getIn(form.values, fieldName).length > 0 &&
134
+ getIn(form.values, fieldName).map(
135
+ (element: any, index: number) => (
136
+ <Box paddingLeft={10}>
137
+ <h4>
138
+ <b>{`${fieldName}.${index}`}</b>
139
+ </h4>
140
+ <Box paddingLeft={10}>
141
+ {formfield.element.type === "object"
142
+ ? parseFields(
143
+ formfield.element.fields,
144
+ `${fieldName}.${index}`
145
+ )
146
+ : generateSingleField(
147
+ formfield.element,
148
+ `${fieldName}.${index}`,
149
+ key
150
+ )}
151
+ </Box>
152
+ <Button
153
+ type="button"
154
+ colorScheme="red"
155
+ marginY={2}
156
+ onClick={() => remove(index)}
157
+ >
158
+ Remove {`${fieldName}.${index}`}
159
+ </Button>
160
+ </Box>
161
+ )
162
+ )}
163
+
164
+ <Button
165
+ type="button"
166
+ marginY={2}
167
+ colorScheme="blue"
168
+ onClick={() => {
169
+ let value = undefined;
170
+
171
+ if (formfield.element.type === "object") {
172
+ value = getInitialValues(formfield.element.fields);
173
+ } else {
174
+ value = getInitialValues([formfield.element])[0];
175
+ }
176
+
177
+ push(value);
178
+ }}
179
+ >
180
+ Add {fieldName}
181
+ </Button>
182
+ </div>
183
+ )}
184
+ </FieldArray>
185
+ );
186
+ } else {
187
+ return generateSingleField(formfield, fieldName, key);
188
+ }
189
+ });
190
+
191
+ return computedFields;
192
+ }
193
+
194
+ function getInitialValues(fields: FormField[] | undefined) {
195
+ let obj: any = {};
196
+
197
+ if (!fields) {
198
+ return obj;
199
+ }
200
+
201
+ fields.forEach((field) => {
202
+ if (field.type === "array") obj[field.name] = [];
203
+ else if (field.type === "object")
204
+ obj[field.name] = getInitialValues(field.fields);
205
+ else if (field.type === "string") obj[field.name] = "";
206
+ else if (field.type === "number") obj[field.name] = 0;
207
+ else obj[field.name] = null;
208
+ });
209
+
210
+ return obj;
211
+ }
212
+
213
+ async function setOptions(fields: FormField[] | undefined) {
214
+ if (!fields) {
215
+ return;
216
+ }
217
+
218
+ const promises = fields.map(async (field) => {
219
+ if (field.type === "dropdown") field.options = await field.getOptions();
220
+ else if (field.type === "array") await setOptions([field.element]);
221
+ else if (field.type === "object") await setOptions(field.fields);
222
+ });
223
+
224
+ await Promise.all(promises);
225
+ }
226
+
227
+ export function GenericForm({ action, onClose, rowId }: GenericFormProps) {
228
+ const [formFields, setFormFields] = useState<any[]>(
229
+ parseFields(action.fields)
230
+ );
231
+ const [initialValues] = useState<any>(getInitialValues(action.fields));
232
+ const [validationSchema] = useState<any>(parseValidation(action.fields));
233
+
234
+ useEffect(() => {
235
+ setOptions(action.fields).then(() => {
236
+ setFormFields(parseFields(action.fields));
237
+ });
238
+ }, []);
239
+
240
+ if (!action.fields) return <></>;
241
+ return (
242
+ <>
243
+ <Formik
244
+ initialValues={initialValues}
245
+ validationSchema={Yup.object(validationSchema)}
246
+ onSubmit={async (values) => {
247
+ try {
248
+ let res;
249
+ if (rowId) res = await action.onSubmit(values, rowId);
250
+ else res = await action.onSubmit(values);
251
+
252
+ if (onClose) onClose();
253
+
254
+ Swal.fire("Ok");
255
+ } catch (e: any) {
256
+ if (onClose) onClose();
257
+ const err = e.request.response;
258
+
259
+ Swal.fire({
260
+ text: "Error: " + err,
261
+ });
262
+ }
263
+ }}
264
+ >
265
+ {({ isSubmitting }) => (
266
+ <Form>
267
+ {formFields}
268
+ <Button
269
+ mt={4}
270
+ colorScheme="teal"
271
+ isLoading={isSubmitting}
272
+ type="submit"
273
+ >
274
+ Submit
275
+ </Button>
276
+ </Form>
277
+ )}
278
+ </Formik>
279
+ </>
280
+ );
281
+ }
@@ -0,0 +1,207 @@
1
+ import { useEffect, useState } from "react";
2
+ import { ChakraProvider } from "@chakra-ui/react";
3
+ import { PageSchema, Action, pageToUrl } from "../schema/schema";
4
+ import {
5
+ Button,
6
+ Modal,
7
+ ModalHeader,
8
+ ModalBody,
9
+ useDisclosure,
10
+ ModalOverlay,
11
+ ModalContent,
12
+ ModalCloseButton,
13
+ Box,
14
+ Stack,
15
+ } from "@chakra-ui/react";
16
+ import { GenericForm } from "./GenericForm";
17
+ import { useNavigate } from "react-router";
18
+ import { ThemeProvider } from "@emotion/react";
19
+ import { createTheme } from "@mui/material/styles";
20
+ import { MaterialReactTable } from "material-react-table";
21
+ import { mkConfig, generateCsv, download } from "export-to-csv";
22
+ import Swal from "sweetalert2";
23
+ import MenuItem from "@mui/material/MenuItem";
24
+
25
+ function Header({ pages, data }: { pages: PageSchema[]; data: any[] }) {
26
+ const nav = useNavigate();
27
+ return (
28
+ <Box background="blue.50" padding={1}>
29
+ <Stack
30
+ margin={3}
31
+ direction="row"
32
+ justifyContent="space-between"
33
+ alignItems="center"
34
+ >
35
+ <Stack margin={4} gap={10} direction="row" alignItems="center">
36
+ {pages.map((p) => (
37
+ <Box as="a" href={pageToUrl(p)}>
38
+ {p.name}
39
+ </Box>
40
+ ))}
41
+ </Stack>
42
+ <Stack direction="row">
43
+ <Button
44
+ colorScheme="red"
45
+ onClick={() => {
46
+ const filename = prompt("Enter filename:");
47
+ if (filename) exportToCSV(data, filename);
48
+ }}
49
+ >
50
+ Export Data
51
+ </Button>
52
+ </Stack>
53
+ </Stack>
54
+ </Box>
55
+ );
56
+ }
57
+
58
+ function exportToCSV(data: any[], filename: string) {
59
+ const formatImageData = (obj: any) => {
60
+ const formattedObj: any = {};
61
+ for (const key in obj) {
62
+ if (typeof obj[key] !== "object") {
63
+ formattedObj[key] = obj[key];
64
+ }
65
+ }
66
+ return formattedObj;
67
+ };
68
+
69
+ const formattedData = data.map(formatImageData);
70
+
71
+ const csvConfig = mkConfig({
72
+ useKeysAsHeaders: true,
73
+ filename: filename,
74
+ });
75
+
76
+ if (formattedData.length > 0) {
77
+ const csv = generateCsv(csvConfig)(formattedData);
78
+ download(csvConfig)(csv);
79
+ } else {
80
+ Swal.fire({
81
+ title: "Error while exporting CSV!",
82
+ text: "There is no data to export.",
83
+ });
84
+ }
85
+ }
86
+
87
+ export function Page({
88
+ pages,
89
+ selectedPage,
90
+ }: {
91
+ pages: PageSchema[];
92
+ selectedPage: PageSchema;
93
+ }) {
94
+ const { isOpen, onOpen, onClose } = useDisclosure();
95
+ const [selectedAction, setSelectedAction] = useState({} as Action);
96
+ const [data, setData] = useState<any[]>([]);
97
+ const [displayData, setDisplayData] = useState<any[][]>([]);
98
+ const [rowId, setRowId] = useState<number>();
99
+
100
+ useEffect(() => {
101
+ selectedPage.getRequest().then((resArray) => {
102
+ setData(resArray);
103
+ });
104
+ }, []);
105
+
106
+ useEffect(() => {
107
+ setDisplayData(
108
+ data.map((table_data) =>
109
+ selectedPage.tableFields.map((field) => field.access(table_data))
110
+ )
111
+ );
112
+ }, [data]);
113
+
114
+ return (
115
+ <>
116
+ <Header pages={pages} data={data} />
117
+
118
+ <ThemeProvider theme={createTheme()}>
119
+ <MaterialReactTable
120
+ columns={selectedPage.tableFields.map((field, index) => ({
121
+ accessorFn: (row: any) => row[index],
122
+ header: field.name,
123
+ }))}
124
+ data={displayData}
125
+ muiTableContainerProps={{ sx: { maxHeight: "70vh" } }}
126
+ enableColumnResizing
127
+ enableColumnPinning
128
+ enableGlobalFilterModes
129
+ enablePagination={true}
130
+ paginationDisplayMode="pages"
131
+ enableRowVirtualization
132
+ rowVirtualizerOptions={{ overscan: 20 }}
133
+ renderTopToolbarCustomActions={() => (
134
+ <ChakraProvider>
135
+ <Stack margin={0} padding={3} direction="row">
136
+ {selectedPage.actions.map((action) => (
137
+ <Button
138
+ colorScheme="blue"
139
+ onClick={() => {
140
+ setSelectedAction(action);
141
+ onOpen();
142
+ }}
143
+ >
144
+ {action.name}
145
+ </Button>
146
+ ))}
147
+ </Stack>
148
+ </ChakraProvider>
149
+ )}
150
+ enableRowActions
151
+ renderRowActionMenuItems={(obj) => {
152
+ return selectedPage.rowActions.map((rowAction) => (
153
+ <MenuItem
154
+ onClick={() => {
155
+ setSelectedAction(rowAction);
156
+ setRowId(obj.row.original[0]);
157
+ onOpen();
158
+
159
+ const rowActionsMenu = document.querySelector(
160
+ ".MuiPaper-root.MuiPopover-paper.MuiMenu-paper"
161
+ );
162
+ if (rowActionsMenu) {
163
+ rowActionsMenu.remove();
164
+ }
165
+ }}
166
+ >
167
+ {rowAction.name + " " + obj.row.original[0]}
168
+ </MenuItem>
169
+ ));
170
+ }}
171
+ />
172
+ </ThemeProvider>
173
+
174
+ <Modal size={"6xl"} isOpen={isOpen} onClose={onClose}>
175
+ <ModalOverlay />
176
+ <ModalContent>
177
+ <ModalHeader>{selectedAction.name}</ModalHeader>
178
+ <ModalCloseButton />
179
+ <ModalBody>
180
+ {selectedAction.fields && selectedAction.fields?.length > 0 ? (
181
+ <GenericForm
182
+ action={selectedAction}
183
+ onClose={onClose}
184
+ rowId={rowId}
185
+ />
186
+ ) : (
187
+ <Button
188
+ colorScheme="blue"
189
+ mr={3}
190
+ onClick={() => {
191
+ selectedAction.onSubmit(rowId);
192
+ onClose();
193
+ window.location.reload();
194
+ }}
195
+ >
196
+ Confirm Action
197
+ </Button>
198
+ )}
199
+ <Button colorScheme="red" mr={3} onClick={onClose}>
200
+ Close
201
+ </Button>
202
+ </ModalBody>
203
+ </ModalContent>
204
+ </Modal>
205
+ </>
206
+ );
207
+ }
package/src/index.css ADDED
@@ -0,0 +1,13 @@
1
+ html, body {
2
+ margin: 0;
3
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
+ sans-serif;
6
+ -webkit-font-smoothing: antialiased;
7
+ -moz-osx-font-smoothing: grayscale;
8
+ }
9
+
10
+ code {
11
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
+ monospace;
13
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import "./index.css";
4
+ import AdminPanel from "./App";
5
+ import reportWebVitals from "./reportWebVitals";
6
+ import { event } from "./pages/event";
7
+
8
+ const root = ReactDOM.createRoot(
9
+ document.getElementById("root") as HTMLElement
10
+ );
11
+ root.render(
12
+ <React.StrictMode>
13
+ <AdminPanel pages={[event]} />
14
+ </React.StrictMode>
15
+ );
16
+
17
+ // If you want to start measuring performance in your app, pass a function
18
+ // to log results (for example: reportWebVitals(console.log))
19
+ // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
20
+ reportWebVitals();
package/src/logo.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>