@kaspernj/api-maker 1.0.412 → 1.0.414

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaspernj/api-maker",
3
- "version": "1.0.412",
3
+ "version": "1.0.414",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "main": "index.js",
@@ -24,6 +24,7 @@
24
24
  "debounce": ">= 2.0.0",
25
25
  "diggerize": ">= 1.0.5",
26
26
  "epic-locks": ">= 1.0.2",
27
+ "file-saver": "^2.0.5",
27
28
  "form-data-objectizer": ">= 1.0.0",
28
29
  "form-serialize": ">= 0.7.2",
29
30
  "format-number": ">= 3.0.0",
@@ -34,6 +35,7 @@
34
35
  "object-to-formdata": ">= 4.1.0",
35
36
  "on-location-changed": ">= 1.0.13",
36
37
  "qs": ">= 6.9.3",
38
+ "react-native-vector-icons": "^10.2.0",
37
39
  "replaceall": ">= 0.1.6",
38
40
  "set-state-compare": "^1.0.49",
39
41
  "spark-md5": "^3.0.2",
@@ -1,6 +1,6 @@
1
1
  import "./style"
2
2
  import BaseComponent from "../../../base-component"
3
- import Icon from "../../../icon"
3
+ import FontAwesomeIcon from "react-native-vector-icons/FontAwesome"
4
4
  import {memo, useRef} from "react"
5
5
  import PropTypes from "prop-types"
6
6
  import PropTypesExact from "prop-types-exact"
@@ -108,11 +108,11 @@ export default memo(shapeComponent(class ApiMakerSuperAdminLayoutHeader extends
108
108
  <View dataSet={{class: "burger-menu-container"}}>
109
109
  {actions &&
110
110
  <Pressable dataSet={{class: "actions-link"}} onPress={this.tt.onGearsClicked} style={{marginRight: 8, fontSize: 22}}>
111
- <Icon icon="gear-solid" />
111
+ <FontAwesomeIcon name="gear-solid" size={20} />
112
112
  </Pressable>
113
113
  }
114
114
  <Pressable dataSet={{class: "burger-menu-link"}} onPress={onTriggerMenu}>
115
- <Icon icon="bars-solid" />
115
+ <FontAwesomeIcon icon="bars" size={20} />
116
116
  </Pressable>
117
117
  </View>
118
118
  </View>
@@ -0,0 +1,119 @@
1
+ import {digs} from "diggerize"
2
+ import * as inflection from "inflection"
3
+ import modelCallbackArgs from "./model-callback-args.mjs"
4
+ import MoneyFormatter from "../money-formatter"
5
+ import {Text} from "react-native"
6
+
7
+ export default class ApiMakerTableColumnContent {
8
+ constructor({column, mode = "react-native", model, table}) {
9
+ this.column = column
10
+ this.mode = mode
11
+ this.model = model
12
+ this.table = table
13
+ }
14
+
15
+ columnContentFromContentArg() {
16
+ const args = modelCallbackArgs(this.table, this.model)
17
+
18
+ args.mode = this.mode
19
+
20
+ const value = this.column.content(args)
21
+
22
+ return this.presentColumnValue(value)
23
+ }
24
+
25
+ columnsContentFromAttributeAndPath() {
26
+ const {attribute: attributeName} = digs(this.column, "attribute")
27
+ const attributeNameUnderscore = inflection.underscore(attributeName)
28
+ const path = this.column.path || []
29
+ let value
30
+ let currentModel = this.model
31
+
32
+ if (path.length > 0) {
33
+ for (const pathPart of path) {
34
+ currentModel = currentModel[pathPart]()
35
+ if (!currentModel) return
36
+ }
37
+ }
38
+
39
+ if (!(attributeName in currentModel)) {
40
+ throw new Error(`${currentModel.constructor.modelName().human()} doesn't respond to ${attributeName}`)
41
+ }
42
+
43
+ if (currentModel.isAttributeLoaded(attributeName)) {
44
+ value = currentModel[attributeName]()
45
+ }
46
+
47
+ const attribute = currentModel.constructor.attributes().find((attribute) => attribute.name() == attributeNameUnderscore)
48
+ const modelColumn = attribute?.getColumn()
49
+
50
+ if (modelColumn?.getType() == "date" && value) {
51
+ const contentText = this.presentDateTime({apiMakerType: "date", value})
52
+
53
+ if (this.mode == "html") {
54
+ return contentText
55
+ } else {
56
+ return (
57
+ <Text>{contentText}</Text>
58
+ )
59
+ }
60
+ }
61
+
62
+ return this.presentColumnValue(value)
63
+ }
64
+
65
+ content() {
66
+ if (this.column.content) {
67
+ return this.columnContentFromContentArg()
68
+ } else if (!this.column.content && this.column.attribute) {
69
+ return this.columnsContentFromAttributeAndPath()
70
+ }
71
+ }
72
+
73
+ presentColumnValue(value) {
74
+ let contentText
75
+
76
+ if (value instanceof Date) {
77
+ contentText = this.presentDateTime({value})
78
+ } else if (MoneyFormatter.isMoney(value)) {
79
+ contentText = MoneyFormatter.format(value)
80
+ } else if (typeof value == "boolean") {
81
+ if (value) {
82
+ contentText = I18n.t("js.shared.yes", {defaultValue: "Yes"})
83
+ } else {
84
+ contentText = I18n.t("js.shared.no", {defaultValue: "No"})
85
+ }
86
+ } else if (Array.isArray(value)) {
87
+ contentText = value
88
+ .map((valuePart) => this.presentColumnValue(valuePart))
89
+ .filter((valuePart) => Boolean(valuePart))
90
+ .join(", ")
91
+
92
+ } else if (typeof value == "string") {
93
+ contentText = value
94
+ } else {
95
+ // Its a React node - just return it and trust the provider to be HTML compatible.
96
+ return value
97
+ }
98
+
99
+ if (this.mode == "html") {
100
+ return contentText
101
+ }
102
+
103
+ return <Text>{contentText}</Text>
104
+ }
105
+
106
+ presentDateTime({apiMakerType, value}) {
107
+ if (!apiMakerType || apiMakerType == "time") {
108
+ const dateTimeFormatName = this.table.props.defaultDateTimeFormatName || "time.formats.default"
109
+
110
+ return I18n.l(dateTimeFormatName, value)
111
+ } else if (apiMakerType == "date") {
112
+ const dateFormatName = this.table.props.defaultDateFormatName || "date.formats.default"
113
+
114
+ return I18n.l(dateFormatName, value)
115
+ } else {
116
+ throw new Error(`Unhandled type: ${apiMakerType}`)
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,10 @@
1
+ import * as inflection from "inflection"
2
+
3
+ export default function modelCallbackArgs(table, model) {
4
+ const modelArgName = inflection.camelize(table.props.modelClass.modelClassData().name, true)
5
+ const modelCallbackArgs = {model}
6
+
7
+ modelCallbackArgs[modelArgName] = model
8
+
9
+ return modelCallbackArgs
10
+ }
@@ -2,13 +2,13 @@ import {Pressable, Text, View} from "react-native"
2
2
  import BaseComponent from "../base-component"
3
3
  import classNames from "classnames"
4
4
  import Column from "./components/column"
5
+ import ColumnContent from "./column-content"
5
6
  import columnIdentifier from "./column-identifier.mjs"
6
7
  import columnVisible from "./column-visible.mjs"
7
- import {digs} from "diggerize"
8
- import Icon from "../icon"
8
+ import FontAwesomeIcon from "react-native-vector-icons/FontAwesome"
9
9
  import * as inflection from "inflection"
10
+ import modelCallbackArgs from "./model-callback-args.mjs"
10
11
  import Link from "../link"
11
- import MoneyFormatter from "../money-formatter"
12
12
  import PropTypes from "prop-types"
13
13
  import propTypesExact from "prop-types-exact"
14
14
  import Row from "./components/row"
@@ -24,20 +24,20 @@ export default memo(shapeComponent(class ApiMakerBootStrapLiveTableModelRow exte
24
24
  index: PropTypes.number.isRequired,
25
25
  isSmallScreen: PropTypes.bool.isRequired,
26
26
  model: PropTypes.object.isRequired,
27
- liveTable: PropTypes.object.isRequired,
27
+ table: PropTypes.object.isRequired,
28
28
  preparedColumns: PropTypes.array,
29
29
  tableSettingFullCacheKey: PropTypes.string.isRequired
30
30
  })
31
31
 
32
32
  render() {
33
- const {index, liveTable, model} = this.p
34
- const {modelClass, workplace} = liveTable.p
35
- const {actionsContent, destroyEnabled, editModelPath, viewModelPath} = liveTable.props
36
- const {columns, currentWorkplace} = liveTable.state
37
- const {styleForColumn, styleForRow} = liveTable.tt
33
+ const {index, table, model} = this.p
34
+ const {modelClass, workplace} = table.p
35
+ const {actionsContent, destroyEnabled, editModelPath, viewModelPath} = table.props
36
+ const {columns, currentWorkplace} = table.state
37
+ const {styleForColumn, styleForRow} = table.tt
38
38
  const even = index % 2 == 0
39
39
 
40
- this.modelCallbackArgs = this._modelCallbackArgs() // 'model' can change so this needs to be re-cached for every render
40
+ this.modelCallbackArgs = modelCallbackArgs(table, model) // 'model' can change so this needs to be re-cached for every render
41
41
 
42
42
  let editPath, viewPath
43
43
 
@@ -65,20 +65,20 @@ export default memo(shapeComponent(class ApiMakerBootStrapLiveTableModelRow exte
65
65
  }
66
66
  {columns && this.columnsContentFromColumns(model, even)}
67
67
  <Column dataSet={{class: "actions-column"}} style={styleForColumn({even, style: {}, type: "actions"})}>
68
- {actionsContent && actionsContent(this.modelCallbackArgs)}
68
+ {actionsContent && actionsContent(this.tt.modelCallbackArgs)}
69
69
  {viewPath &&
70
- <Link dataSet={{class: "view-button"}} to={viewPath}>
71
- <Icon icon="magnifying-glass-solid" />
70
+ <Link dataSet={{class: "view-button"}} style={{marginLeft: 2, marginRight: 2}} to={viewPath}>
71
+ <FontAwesomeIcon name="search" size={18} />
72
72
  </Link>
73
73
  }
74
74
  {editPath &&
75
- <Link dataSet={{class: "edit-button"}} to={editPath}>
76
- <Icon icon="pen-solid" />
75
+ <Link dataSet={{class: "edit-button"}} style={{marginLeft: 2, marginRight: 2}} to={editPath}>
76
+ <FontAwesomeIcon name="pencil" size={20} />
77
77
  </Link>
78
78
  }
79
79
  {destroyEnabled && model.can("destroy") &&
80
- <Pressable dataSet={{class: "destroy-button"}} onPress={this.tt.onDestroyClicked}>
81
- <Icon icon="xmark-solid" />
80
+ <Pressable dataSet={{class: "destroy-button"}} style={{marginLeft: 2, marginRight: 2}} onPress={this.tt.onDestroyClicked}>
81
+ <FontAwesomeIcon name="remove" size={22} />
82
82
  </Pressable>
83
83
  }
84
84
  </Column>
@@ -96,7 +96,7 @@ export default memo(shapeComponent(class ApiMakerBootStrapLiveTableModelRow exte
96
96
  }
97
97
 
98
98
  columnsContentFromColumns(model, even) {
99
- const {isSmallScreen, liveTable, preparedColumns} = this.p
99
+ const {isSmallScreen, table, preparedColumns} = this.p
100
100
 
101
101
  return preparedColumns?.map(({column, tableSettingColumn, width}, columnIndex) => columnVisible(column, tableSettingColumn) &&
102
102
  <Column
@@ -105,74 +105,25 @@ export default memo(shapeComponent(class ApiMakerBootStrapLiveTableModelRow exte
105
105
  identifier: columnIdentifier(column)
106
106
  }}
107
107
  key={columnIdentifier(column)}
108
- style={liveTable.styleForColumn({column, columnIndex, even, style: {width: `${width}%`}})}
109
- {...liveTable.columnProps(column)}
108
+ style={table.styleForColumn({column, columnIndex, even, style: {width: `${width}%`}})}
109
+ {...table.columnProps(column)}
110
110
  >
111
111
  {isSmallScreen &&
112
112
  <View dataSet={{class: "table--column-label"}}>
113
113
  <Text>
114
- {liveTable.headerLabelForColumn(column)}
114
+ {table.headerLabelForColumn(column)}
115
115
  </Text>
116
116
  </View>
117
117
  }
118
118
  <View dataSet={{class: "table--column-value"}}>
119
- {column.content && this.columnContentFromContentArg(column, model)}
120
- {!column.content && column.attribute && this.columnsContentFromAttributeAndPath(column, model)}
119
+ {new ColumnContent({column, model, table}).content()}
121
120
  </View>
122
121
  </Column>
123
122
  )
124
123
  }
125
124
 
126
- columnContentFromContentArg (column, _model) {
127
- const value = column.content(this.modelCallbackArgs)
128
-
129
- return this.presentColumnValue(value)
130
- }
131
-
132
- columnsContentFromAttributeAndPath (column, model) {
133
- const {attribute: attributeName} = digs(column, "attribute")
134
- const attributeNameUnderscore = inflection.underscore(attributeName)
135
- const path = column.path || []
136
- let value
137
- let currentModel = model
138
-
139
- if (path.length > 0) {
140
- for (const pathPart of path) {
141
- currentModel = currentModel[pathPart]()
142
- if (!currentModel) return
143
- }
144
- }
145
-
146
- if (!(attributeName in currentModel)) {
147
- throw new Error(`${currentModel.constructor.modelName().human()} doesn't respond to ${attributeName}`)
148
- }
149
-
150
- if (currentModel.isAttributeLoaded(attributeName)) value = currentModel[attributeName]()
151
-
152
- const attribute = currentModel.constructor.attributes().find((attribute) => attribute.name() == attributeNameUnderscore)
153
- const modelColumn = attribute?.getColumn()
154
-
155
- if (modelColumn?.getType() == "date" && value) {
156
- return (
157
- <Text>{this.presentDateTime({apiMakerType: "date", value})}</Text>
158
- )
159
- }
160
-
161
- return this.presentColumnValue(value)
162
- }
163
-
164
- _modelCallbackArgs () {
165
- const {model} = this.p
166
- const modelArgName = inflection.camelize(this.props.liveTable.props.modelClass.modelClassData().name, true)
167
- const modelCallbackArgs = {model}
168
-
169
- modelCallbackArgs[modelArgName] = model
170
-
171
- return modelCallbackArgs
172
- }
173
-
174
125
  onDestroyClicked = async () => {
175
- const {destroyMessage} = this.p.liveTable.props
126
+ const {destroyMessage} = this.p.table.props
176
127
  const {model} = this.p
177
128
 
178
129
  if (!confirm(I18n.t("js.shared.are_you_sure"))) {
@@ -189,46 +140,4 @@ export default memo(shapeComponent(class ApiMakerBootStrapLiveTableModelRow exte
189
140
  FlashMessage.errorResponse(error)
190
141
  }
191
142
  }
192
-
193
- presentColumnValue(value) {
194
- if (value instanceof Date) {
195
- return <Text>{this.presentDateTime({value})}</Text>
196
- } else if (MoneyFormatter.isMoney(value)) {
197
- return <Text>{MoneyFormatter.format(value)}</Text>
198
- } else if (typeof value == "boolean") {
199
- if (value) {
200
- return <Text>{I18n.t("js.shared.yes", {defaultValue: "Yes"})}</Text>
201
- }
202
-
203
- return <Text>{I18n.t("js.shared.no", {defaultValue: "No"})}</Text>
204
- } else if (Array.isArray(value)) {
205
- return (
206
- <Text>
207
- {value
208
- .map((valuePart) => this.presentColumnValue(valuePart))
209
- .filter((valuePart) => Boolean(valuePart))
210
- .join(", ")
211
- }
212
- </Text>
213
- )
214
- } else if (typeof value == "string") {
215
- return <Text>{value}</Text>
216
- }
217
-
218
- return <Text>{value}</Text>
219
- }
220
-
221
- presentDateTime({apiMakerType, value}) {
222
- if (!apiMakerType || apiMakerType == "time") {
223
- const dateTimeFormatName = this.props.liveTable.props.defaultDateTimeFormatName || "time.formats.default"
224
-
225
- return I18n.l(dateTimeFormatName, value)
226
- } else if (apiMakerType == "date") {
227
- const dateFormatName = this.props.liveTable.props.defaultDateFormatName || "date.formats.default"
228
-
229
- return I18n.l(dateFormatName, value)
230
- } else {
231
- throw new Error(`Unhandled type: ${apiMakerType}`)
232
- }
233
- }
234
143
  }))
@@ -0,0 +1,66 @@
1
+ import BaseComponent from "../../base-component"
2
+ import ColumnContent from "../column-content"
3
+ import columnIdentifier from "../column-identifier.mjs"
4
+ import columnVisible from "../column-visible.mjs"
5
+ import {saveAs} from "file-saver"
6
+ import FontAwesomeIcon from "react-native-vector-icons/FontAwesome"
7
+ import {memo} from "react"
8
+ import PropTypes from "prop-types"
9
+ import propTypesExact from "prop-types-exact"
10
+ import {renderToString} from "react-dom/server"
11
+ import {shapeComponent} from "set-state-compare/src/shape-component.js"
12
+ import {Pressable, Text} from "react-native"
13
+
14
+ export default memo(shapeComponent(class ApiMakerTableSettingsDownloadAction extends BaseComponent {
15
+ static propTypes = propTypesExact({
16
+ table: PropTypes.object.isRequired
17
+ })
18
+
19
+ render() {
20
+ return (
21
+ <Pressable onPress={this.tt.onDownloadPress} style={{flexDirection: "row", alignItems: "center"}}>
22
+ <FontAwesomeIcon name="download" size={20} />
23
+ <Text style={{marginLeft: 5}}>
24
+ Download
25
+ </Text>
26
+ </Pressable>
27
+ )
28
+ }
29
+
30
+ onDownloadPress = () => {
31
+ const {table} = this.p
32
+ const {modelClass} = table.p
33
+ const {collection} = table.tt
34
+ const {models} = collection
35
+ const {preparedColumns} = table.s
36
+ const tableElement = (
37
+ <table>
38
+ <thead>
39
+ <tr>
40
+ {preparedColumns?.map(({column, tableSettingColumn}) => columnVisible(column, tableSettingColumn) &&
41
+ <th key={columnIdentifier(column)}>
42
+ {table.headerLabelForColumn(column)}
43
+ </th>
44
+ )}
45
+ </tr>
46
+ </thead>
47
+ <tbody>
48
+ {models.map((model) =>
49
+ <tr key={model.id()}>
50
+ {preparedColumns?.map(({column, tableSettingColumn}) => columnVisible(column, tableSettingColumn) &&
51
+ <td key={columnIdentifier(column)}>
52
+ {new ColumnContent({column, mode: "html", model, table}).content()}
53
+ </td>
54
+ )}
55
+ </tr>
56
+ )}
57
+ </tbody>
58
+ </table>
59
+ )
60
+ const tableHTML = renderToString(tableElement)
61
+ const blob = new Blob([tableHTML], {type: "text/html;charset=utf-8"})
62
+ const fileName = `${modelClass.modelName().human({count: 2})}.html`
63
+
64
+ saveAs(blob, fileName)
65
+ }
66
+ }))
@@ -1,6 +1,7 @@
1
1
  import BaseComponent from "../../base-component"
2
2
  import columnIdentifier from "../column-identifier.mjs"
3
3
  import ColumnRow from "./column-row"
4
+ import DownloadAction from "./download-action"
4
5
  import {memo, useRef} from "react"
5
6
  import PropTypes from "prop-types"
6
7
  import propTypesExact from "prop-types-exact"
@@ -46,6 +47,7 @@ export default memo(shapeComponent(class ApiMakerTableSettings extends BaseCompo
46
47
  Settings
47
48
  </Text>
48
49
  </View>
50
+ <DownloadAction table={table} />
49
51
  <View style={{marginBottom: 5}}>
50
52
  <Text style={{fontSize: 16, fontWeight: "bold"}}>
51
53
  Columns
@@ -9,9 +9,9 @@ import columnVisible from "./column-visible.mjs"
9
9
  import debounce from "debounce"
10
10
  import Filters from "./filters"
11
11
  import FlatList from "./components/flat-list"
12
+ import FontAwesomeIcon from "react-native-vector-icons/FontAwesome"
12
13
  import Header from "./components/header"
13
14
  import HeaderColumn from "./header-column"
14
- import Icon from "../icon"
15
15
  import * as inflection from "inflection"
16
16
  import modelClassRequire from "../model-class-require.mjs"
17
17
  import ModelRow from "./model-row"
@@ -480,9 +480,9 @@ export default memo(shapeComponent(class ApiMakerTable extends BaseComponent {
480
480
  index={index}
481
481
  isSmallScreen={this.tt.isSmallScreen}
482
482
  key={model.id()}
483
- liveTable={this}
484
483
  model={model}
485
484
  preparedColumns={preparedColumns}
485
+ table={this}
486
486
  tableSettingFullCacheKey={tableSettingFullCacheKey}
487
487
  />
488
488
  )
@@ -556,14 +556,14 @@ export default memo(shapeComponent(class ApiMakerTable extends BaseComponent {
556
556
  <View style={{flexDirection: "row"}}>
557
557
  {controls && controls({models, qParams, query, result})}
558
558
  <Pressable dataSet={{class: "filter-button"}} onPress={this.tt.onFilterClicked}>
559
- <Icon icon="magnifying-glass-solid" />
559
+ <FontAwesomeIcon name="search" size={20} />
560
560
  </Pressable>
561
561
  <View style={{position: "relative"}}>
562
562
  {showSettings &&
563
563
  <Settings onRequestClose={this.tt.onRequestCloseSettings} table={this} />
564
564
  }
565
565
  <Pressable dataSet={{class: "settings-button"}} onPress={this.tt.onSettingsClicked}>
566
- <Icon icon="gear-solid" />
566
+ <FontAwesomeIcon name="gear" size={20} />
567
567
  </Pressable>
568
568
  </View>
569
569
  </View>
package/src/icon.jsx DELETED
@@ -1,51 +0,0 @@
1
- import BaseComponent from "./base-component"
2
- import {Image} from "react-native"
3
- import {memo, useMemo} from "react"
4
- import PropTypes from "prop-types"
5
- import {shapeComponent} from "set-state-compare/src/shape-component.js"
6
-
7
- export default memo(shapeComponent(class ComponentsIcon extends BaseComponent {
8
- static propTypes = {
9
- icon: PropTypes.string.isRequired
10
- }
11
-
12
- setup() {
13
- const {icon} = this.p
14
-
15
- this.useStates({
16
- IconComponent: null,
17
- imageSource: null
18
- })
19
-
20
- useMemo(() => {
21
- this.loadIcon()
22
- }, [icon])
23
- }
24
-
25
- async loadIcon() {
26
- const {icon} = this.p
27
- const IconComponent = await import(`./icons/${icon}.svg`)
28
-
29
- this.setState({IconComponent})
30
- }
31
-
32
- render() {
33
- const {icon, style, ...restProps} = this.props
34
- const {IconComponent} = this.s
35
- const actualStyle = Object.assign(
36
- {
37
- width: 16,
38
- height: 16
39
- },
40
- style
41
- )
42
-
43
- if (!IconComponent) {
44
- return null
45
- }
46
-
47
- return (
48
- <Image source={IconComponent.default} style={actualStyle} {...restProps} />
49
- )
50
- }
51
- }))
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M0 96C0 78.3 14.3 64 32 64l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 128C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 288c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32L32 448c-17.7 0-32-14.3-32-32s14.3-32 32-32l384 0c17.7 0 32 14.3 32 32z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>