@sanity/dashboard 2.30.1 → 3.0.0-sanity-v3.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.
- package/README.md +123 -44
- package/lib/cjs/index.js +1002 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/esm/index.js +977 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/types/index.d.ts +42 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +53 -14
- package/src/components/DashboardLayout.tsx +10 -0
- package/src/components/DashboardWidgetContainer.tsx +69 -0
- package/src/components/NotFoundWidget.tsx +30 -0
- package/src/components/WidgetGroup.tsx +108 -0
- package/src/containers/Dashboard.tsx +19 -0
- package/src/containers/DashboardContext.tsx +8 -0
- package/src/containers/WidgetContainer.tsx +21 -0
- package/src/index.tsx +62 -0
- package/src/types.ts +21 -0
- package/src/versionedClient.ts +7 -0
- package/src/widgets/projectInfo/ProjectInfo.tsx +233 -0
- package/src/widgets/projectInfo/index.ts +10 -0
- package/src/widgets/projectUsers/ProjectUsers.tsx +171 -0
- package/src/widgets/projectUsers/index.ts +10 -0
- package/src/widgets/sanityTutorials/SanityTutorials.tsx +77 -0
- package/src/widgets/sanityTutorials/Tutorial.tsx +111 -0
- package/src/widgets/sanityTutorials/dataAdapter.ts +48 -0
- package/src/widgets/sanityTutorials/index.ts +10 -0
- package/.babelrc +0 -4
- package/lib/DashboardTool.js +0 -59
- package/lib/components/DashboardLayout.js +0 -35
- package/lib/components/NotFoundWidget.js +0 -51
- package/lib/components/WidgetGroup.js +0 -67
- package/lib/components/dashboardWidget.js +0 -51
- package/lib/containers/Dashboard.js +0 -32
- package/lib/containers/WidgetContainer.js +0 -56
- package/lib/dashboardConfig.js +0 -16
- package/lib/legacyParts.js +0 -55
- package/lib/versionedClient.js +0 -20
- package/lib/widget.css +0 -62
- package/lib/widgets/projectInfo/ProjectInfo.js +0 -265
- package/lib/widgets/projectInfo/index.js +0 -19
- package/lib/widgets/projectUsers/ProjectUsers.js +0 -188
- package/lib/widgets/projectUsers/index.js +0 -16
- package/lib/widgets/sanityTutorials/SanityTutorials.js +0 -115
- package/lib/widgets/sanityTutorials/Tutorial.js +0 -111
- package/lib/widgets/sanityTutorials/dataAdapter.js +0 -28
- package/lib/widgets/sanityTutorials/index.js +0 -19
- package/sanity.json +0 -59
- package/tsconfig.json +0 -17
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import React, {useEffect, useMemo, useState} from 'react'
|
|
2
|
+
import {Box, Card, Stack, Heading, Grid, Label, Text, Code, Button} from '@sanity/ui'
|
|
3
|
+
import {useVersionedClient} from '../../versionedClient'
|
|
4
|
+
import {Subscription} from 'rxjs'
|
|
5
|
+
import {WidgetContainer} from '../../containers/WidgetContainer'
|
|
6
|
+
import {DashboardWidgetContainer} from '../../components/DashboardWidgetContainer'
|
|
7
|
+
import {DashboardWidget} from '../../types'
|
|
8
|
+
|
|
9
|
+
export interface ProjectInfoProps {
|
|
10
|
+
__experimental_before?: DashboardWidget[]
|
|
11
|
+
data: ProjectData[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface App {
|
|
15
|
+
title: string
|
|
16
|
+
rows?: App[]
|
|
17
|
+
value?: string | {error: string}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ProjectData {
|
|
21
|
+
title: string
|
|
22
|
+
category?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isUrl(url?: string) {
|
|
26
|
+
return url && /^https?:\/\//.test(`${url}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getGraphQlUrl(projectId: string, dataset: string) {
|
|
30
|
+
return `https://${projectId}.api.sanity.io/v1/graphql/${dataset}/default`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getGroqUrl(projectId: string, dataset: string) {
|
|
34
|
+
return `https://${projectId}.api.sanity.io/v1/groq/${dataset}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getManageUrl(projectId: string) {
|
|
38
|
+
return `https://manage.sanity.io/projects/${projectId}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const NO_EXPERIMENTAL: DashboardWidget[] = []
|
|
42
|
+
const NO_DATA: ProjectData[] = []
|
|
43
|
+
|
|
44
|
+
export function ProjectInfo(props: ProjectInfoProps) {
|
|
45
|
+
const {__experimental_before = NO_EXPERIMENTAL, data = NO_DATA} = props
|
|
46
|
+
const [studioHost, setStudioHost] = useState<string | {error: string} | undefined>()
|
|
47
|
+
const [graphqlApi, setGraphQlApi] = useState<string | {error: string} | undefined>()
|
|
48
|
+
const versionedClient = useVersionedClient()
|
|
49
|
+
const {projectId = 'unknown', dataset = 'unknown'} = versionedClient.config()
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const subscriptions: Subscription[] = []
|
|
53
|
+
|
|
54
|
+
subscriptions.push(
|
|
55
|
+
versionedClient.observable
|
|
56
|
+
.request<{studioHost: string}>({uri: `/projects/${projectId}`})
|
|
57
|
+
.subscribe({
|
|
58
|
+
next: (result) => {
|
|
59
|
+
const {studioHost: host} = result
|
|
60
|
+
setStudioHost(host ? `https://${host}.sanity.studio` : undefined)
|
|
61
|
+
},
|
|
62
|
+
error: (error) => {
|
|
63
|
+
console.error('Error while looking for studioHost', error)
|
|
64
|
+
setStudioHost({
|
|
65
|
+
error: 'Something went wrong while looking up studioHost. See console.',
|
|
66
|
+
})
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// ping assumed graphql endpoint
|
|
72
|
+
subscriptions.push(
|
|
73
|
+
versionedClient.observable
|
|
74
|
+
.request({
|
|
75
|
+
method: 'HEAD',
|
|
76
|
+
uri: `/graphql/${dataset}/default`,
|
|
77
|
+
})
|
|
78
|
+
.subscribe({
|
|
79
|
+
next: () => setGraphQlApi(getGraphQlUrl(projectId, dataset)),
|
|
80
|
+
error: (error) => {
|
|
81
|
+
if (error.statusCode === 404) {
|
|
82
|
+
setGraphQlApi(undefined)
|
|
83
|
+
} else {
|
|
84
|
+
console.error('Error while looking for graphqlApi', error)
|
|
85
|
+
setGraphQlApi({
|
|
86
|
+
error: 'Something went wrong while looking up graphqlApi. See console.',
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
subscriptions.forEach((s) => s.unsubscribe())
|
|
95
|
+
}
|
|
96
|
+
}, [dataset, projectId, versionedClient, setGraphQlApi, setStudioHost])
|
|
97
|
+
|
|
98
|
+
const assembleTableRows = useMemo(() => {
|
|
99
|
+
let result: App[] = [
|
|
100
|
+
{
|
|
101
|
+
title: 'Sanity project',
|
|
102
|
+
rows: [
|
|
103
|
+
{title: 'Project ID', value: projectId},
|
|
104
|
+
{title: 'Dataset', value: dataset},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
// Handle any apps
|
|
110
|
+
const apps: App[] = [
|
|
111
|
+
studioHost ? {title: 'Studio', value: studioHost} : null,
|
|
112
|
+
...data.filter((item) => item.category === 'apps'),
|
|
113
|
+
].filter((a): a is App => !!a)
|
|
114
|
+
if (apps.length > 0) {
|
|
115
|
+
result = result.concat([{title: 'Apps', rows: apps}])
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle APIs
|
|
119
|
+
result = result.concat(
|
|
120
|
+
[
|
|
121
|
+
{
|
|
122
|
+
title: 'APIs',
|
|
123
|
+
rows: [
|
|
124
|
+
{title: 'GROQ', value: getGroqUrl(projectId, dataset)},
|
|
125
|
+
{
|
|
126
|
+
title: 'GraphQL',
|
|
127
|
+
value: (typeof graphqlApi === 'object' ? 'Error' : graphqlApi) ?? 'Not deployed',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
data.filter((item) => item.category === 'apis')
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// Handle whatever else there might be
|
|
136
|
+
const otherStuff: Record<string, ProjectData[]> = {}
|
|
137
|
+
data.forEach((item) => {
|
|
138
|
+
if (item.category && item.category !== 'apps' && item.category !== 'apis') {
|
|
139
|
+
if (!otherStuff[item.category]) {
|
|
140
|
+
otherStuff[item.category] = []
|
|
141
|
+
}
|
|
142
|
+
otherStuff[item.category].push(item)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
Object.keys(otherStuff).forEach((category) => {
|
|
146
|
+
result.push({title: category, rows: otherStuff[category]})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return result
|
|
150
|
+
}, [graphqlApi, studioHost, projectId, dataset, data])
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
{__experimental_before.map((widgetConfig, idx) => (
|
|
155
|
+
<WidgetContainer key={idx} {...widgetConfig} />
|
|
156
|
+
))}
|
|
157
|
+
<Box height="fill" marginTop={__experimental_before?.length > 0 ? 4 : 0}>
|
|
158
|
+
<DashboardWidgetContainer
|
|
159
|
+
footer={
|
|
160
|
+
<Button
|
|
161
|
+
style={{width: '100%'}}
|
|
162
|
+
paddingX={2}
|
|
163
|
+
paddingY={4}
|
|
164
|
+
mode="bleed"
|
|
165
|
+
tone="primary"
|
|
166
|
+
text="Manage project"
|
|
167
|
+
as="a"
|
|
168
|
+
href={getManageUrl(projectId)}
|
|
169
|
+
/>
|
|
170
|
+
}
|
|
171
|
+
>
|
|
172
|
+
<Card
|
|
173
|
+
paddingY={4}
|
|
174
|
+
radius={2}
|
|
175
|
+
role="table"
|
|
176
|
+
aria-label="Project info"
|
|
177
|
+
aria-describedby="project_info_table"
|
|
178
|
+
>
|
|
179
|
+
<Stack space={4}>
|
|
180
|
+
<Box paddingX={3} as="header">
|
|
181
|
+
<Heading size={1} as="h2" id="project_info_table">
|
|
182
|
+
Project info
|
|
183
|
+
</Heading>
|
|
184
|
+
</Box>
|
|
185
|
+
{assembleTableRows.map((item) => {
|
|
186
|
+
if (!item || !item.rows) {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<Stack key={item.title} space={3}>
|
|
192
|
+
<Card borderBottom padding={3}>
|
|
193
|
+
<Label size={0} muted role="columnheader">
|
|
194
|
+
{item.title}
|
|
195
|
+
</Label>
|
|
196
|
+
</Card>
|
|
197
|
+
<Stack space={4} paddingX={3} role="rowgroup">
|
|
198
|
+
{item.rows.map((row) => {
|
|
199
|
+
return (
|
|
200
|
+
<Grid key={row.title} columns={2} role="row">
|
|
201
|
+
<Text weight="medium" role="rowheader">
|
|
202
|
+
{row.title}
|
|
203
|
+
</Text>
|
|
204
|
+
{typeof row.value === 'object' && (
|
|
205
|
+
<Text size={1}>{row.value?.error}</Text>
|
|
206
|
+
)}
|
|
207
|
+
{typeof row.value === 'string' && (
|
|
208
|
+
<>
|
|
209
|
+
{isUrl(row.value) ? (
|
|
210
|
+
<Text size={1} role="cell" style={{wordBreak: 'break-word'}}>
|
|
211
|
+
<a href={row.value}>{row.value}</a>
|
|
212
|
+
</Text>
|
|
213
|
+
) : (
|
|
214
|
+
<Code size={1} role="cell" style={{wordBreak: 'break-word'}}>
|
|
215
|
+
{row.value}
|
|
216
|
+
</Code>
|
|
217
|
+
)}
|
|
218
|
+
</>
|
|
219
|
+
)}
|
|
220
|
+
</Grid>
|
|
221
|
+
)
|
|
222
|
+
})}
|
|
223
|
+
</Stack>
|
|
224
|
+
</Stack>
|
|
225
|
+
)
|
|
226
|
+
})}
|
|
227
|
+
</Stack>
|
|
228
|
+
</Card>
|
|
229
|
+
</DashboardWidgetContainer>
|
|
230
|
+
</Box>
|
|
231
|
+
</>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {ProjectInfo} from './ProjectInfo'
|
|
2
|
+
import {LayoutConfig, DashboardWidget} from '../../types'
|
|
3
|
+
|
|
4
|
+
export function projectInfoWidget(config?: {layout?: LayoutConfig}): DashboardWidget {
|
|
5
|
+
return {
|
|
6
|
+
name: 'project-info',
|
|
7
|
+
component: ProjectInfo,
|
|
8
|
+
layout: config?.layout ?? {width: 'medium'},
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React, {useCallback, useEffect, useState} from 'react'
|
|
2
|
+
import {map, switchMap} from 'rxjs/operators'
|
|
3
|
+
import {Stack, Spinner, Card, Box, Text, Button} from '@sanity/ui'
|
|
4
|
+
import {RobotIcon} from '@sanity/icons'
|
|
5
|
+
import styled from 'styled-components'
|
|
6
|
+
import {useSource} from 'sanity'
|
|
7
|
+
import {DefaultPreview} from 'sanity/_unstable'
|
|
8
|
+
import {useVersionedClient} from '../../versionedClient'
|
|
9
|
+
import {User} from '@sanity/types'
|
|
10
|
+
import {DashboardWidgetContainer} from '../../components/DashboardWidgetContainer'
|
|
11
|
+
|
|
12
|
+
const AvatarWrapper = styled(Card)`
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
border-radius: 50%;
|
|
15
|
+
border-color: transparent;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
|
|
20
|
+
& > img {
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: auto;
|
|
23
|
+
}
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
function getInviteUrl(projectId: string) {
|
|
27
|
+
return `https://manage.sanity.io/projects/${projectId}/team/invite`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Member {
|
|
31
|
+
id: string
|
|
32
|
+
role: string
|
|
33
|
+
isRobot: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Project {
|
|
37
|
+
id: string
|
|
38
|
+
members: Member[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ProjectUsers() {
|
|
42
|
+
const [project, setProject] = useState<Project | undefined>()
|
|
43
|
+
const [users, setUsers] = useState<User[] | undefined>()
|
|
44
|
+
const [error, setError] = useState<Error | undefined>()
|
|
45
|
+
|
|
46
|
+
const versionedClient = useVersionedClient()
|
|
47
|
+
const {
|
|
48
|
+
__internal: {userStore},
|
|
49
|
+
} = useSource()
|
|
50
|
+
|
|
51
|
+
const fetchData = useCallback(() => {
|
|
52
|
+
const {projectId} = versionedClient.config()
|
|
53
|
+
const subscription = versionedClient.observable
|
|
54
|
+
.request<Project>({
|
|
55
|
+
uri: `/projects/${projectId}`,
|
|
56
|
+
})
|
|
57
|
+
.pipe(
|
|
58
|
+
switchMap((_project) =>
|
|
59
|
+
userStore.observable
|
|
60
|
+
.getUsers(_project.members.map((mem) => mem.id))
|
|
61
|
+
.pipe(map((_users) => ({project: _project, users: _users})))
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
.subscribe({
|
|
65
|
+
next: ({users: _users, project: _project}) => {
|
|
66
|
+
setProject(_project)
|
|
67
|
+
setUsers(
|
|
68
|
+
(Array.isArray(_users) ? _users : [_users]).sort((userA, userB) =>
|
|
69
|
+
sortUsersByRobotStatus(userA, userB, _project)
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
},
|
|
73
|
+
error: (e: Error) => setError(e),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return () => subscription.unsubscribe()
|
|
77
|
+
}, [userStore, versionedClient])
|
|
78
|
+
|
|
79
|
+
useEffect(() => fetchData(), [fetchData])
|
|
80
|
+
|
|
81
|
+
const handleRetryFetch = useCallback(() => fetchData(), [fetchData])
|
|
82
|
+
|
|
83
|
+
const isLoading = !users || !project
|
|
84
|
+
|
|
85
|
+
if (error) {
|
|
86
|
+
return (
|
|
87
|
+
<DashboardWidgetContainer header="Project users">
|
|
88
|
+
<Box padding={4}>
|
|
89
|
+
<Text>
|
|
90
|
+
Something went wrong while fetching data. You could{' '}
|
|
91
|
+
<a onClick={handleRetryFetch} title="Retry users fetch" style={{cursor: 'pointer'}}>
|
|
92
|
+
retry
|
|
93
|
+
</a>
|
|
94
|
+
..?
|
|
95
|
+
</Text>
|
|
96
|
+
</Box>
|
|
97
|
+
</DashboardWidgetContainer>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<DashboardWidgetContainer
|
|
103
|
+
header="Project users"
|
|
104
|
+
footer={
|
|
105
|
+
<Button
|
|
106
|
+
style={{width: '100%'}}
|
|
107
|
+
paddingX={2}
|
|
108
|
+
paddingY={4}
|
|
109
|
+
mode="bleed"
|
|
110
|
+
tone="primary"
|
|
111
|
+
text="Invite members"
|
|
112
|
+
as="a"
|
|
113
|
+
loading={isLoading}
|
|
114
|
+
href={isLoading ? undefined : getInviteUrl(project.id)}
|
|
115
|
+
/>
|
|
116
|
+
}
|
|
117
|
+
>
|
|
118
|
+
{isLoading && (
|
|
119
|
+
<Box paddingY={5} paddingX={2}>
|
|
120
|
+
<Stack space={4}>
|
|
121
|
+
<Text align="center" muted size={1}>
|
|
122
|
+
<Spinner />
|
|
123
|
+
</Text>
|
|
124
|
+
<Text align="center" size={1} muted>
|
|
125
|
+
Loading items...
|
|
126
|
+
</Text>
|
|
127
|
+
</Stack>
|
|
128
|
+
</Box>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{!isLoading && (
|
|
132
|
+
<Stack space={3} padding={3}>
|
|
133
|
+
{users?.map((user) => {
|
|
134
|
+
const membership = project.members.find((member) => member.id === user.id)
|
|
135
|
+
const media = membership?.isRobot ? (
|
|
136
|
+
<Text size={3}>
|
|
137
|
+
<RobotIcon />
|
|
138
|
+
</Text>
|
|
139
|
+
) : (
|
|
140
|
+
<AvatarWrapper tone="transparent">
|
|
141
|
+
{user?.imageUrl && <img src={user.imageUrl} alt={user?.displayName} />}
|
|
142
|
+
</AvatarWrapper>
|
|
143
|
+
)
|
|
144
|
+
return (
|
|
145
|
+
<Box key={user.id}>
|
|
146
|
+
<DefaultPreview
|
|
147
|
+
title={user.displayName}
|
|
148
|
+
subtitle={membership?.role}
|
|
149
|
+
media={media}
|
|
150
|
+
/>
|
|
151
|
+
</Box>
|
|
152
|
+
)
|
|
153
|
+
})}
|
|
154
|
+
</Stack>
|
|
155
|
+
)}
|
|
156
|
+
</DashboardWidgetContainer>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sortUsersByRobotStatus(userA: User, userB: User, project: Project) {
|
|
161
|
+
const {members} = project
|
|
162
|
+
const membershipA = members.find((member) => member.id === userA?.id)
|
|
163
|
+
const membershipB = members.find((member) => member.id === userB?.id)
|
|
164
|
+
if (membershipA?.isRobot) {
|
|
165
|
+
return 1
|
|
166
|
+
}
|
|
167
|
+
if (membershipB?.isRobot) {
|
|
168
|
+
return -1
|
|
169
|
+
}
|
|
170
|
+
return 0
|
|
171
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {ProjectUsers} from './ProjectUsers'
|
|
2
|
+
import {LayoutConfig, DashboardWidget} from '../../types'
|
|
3
|
+
|
|
4
|
+
export function projectUsersWidget(config?: {layout?: LayoutConfig}): DashboardWidget {
|
|
5
|
+
return {
|
|
6
|
+
name: 'project-info',
|
|
7
|
+
component: ProjectUsers,
|
|
8
|
+
layout: config?.layout,
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React, {useEffect, useState} from 'react'
|
|
2
|
+
import {Flex} from '@sanity/ui'
|
|
3
|
+
import {Tutorial} from './Tutorial'
|
|
4
|
+
import {FeedItem, Guide, useDataAdapter} from './dataAdapter'
|
|
5
|
+
import {DashboardWidgetContainer} from '../../components/DashboardWidgetContainer'
|
|
6
|
+
|
|
7
|
+
function createUrl(slug: {current: string}, type?: string) {
|
|
8
|
+
if (type === 'tutorial') {
|
|
9
|
+
return `https://www.sanity.io/docs/tutorials/${slug.current}`
|
|
10
|
+
} else if (type === 'guide') {
|
|
11
|
+
return `https://www.sanity.io/docs/guides/${slug.current}`
|
|
12
|
+
}
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SanityTutorialsProps {
|
|
17
|
+
templateRepoId: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SanityTutorials(props: SanityTutorialsProps) {
|
|
21
|
+
const {templateRepoId} = props
|
|
22
|
+
const [feedItems, setFeedItems] = useState<FeedItem[]>([])
|
|
23
|
+
|
|
24
|
+
const {getFeed, urlBuilder} = useDataAdapter()
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const subscription = getFeed(templateRepoId).subscribe((response) => {
|
|
28
|
+
setFeedItems(response.items)
|
|
29
|
+
})
|
|
30
|
+
return () => {
|
|
31
|
+
subscription.unsubscribe()
|
|
32
|
+
}
|
|
33
|
+
}, [setFeedItems, getFeed, templateRepoId])
|
|
34
|
+
|
|
35
|
+
const title = 'Learn about Sanity'
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<DashboardWidgetContainer header={title}>
|
|
39
|
+
<Flex as="ul" overflow="auto" align="stretch" paddingY={2}>
|
|
40
|
+
{feedItems?.map((feedItem, index) => {
|
|
41
|
+
if (!feedItem.title || (!feedItem.guideOrTutorial && !feedItem.externalLink)) {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
const presenter = feedItem.presenter || feedItem.guideOrTutorial?.presenter || {}
|
|
45
|
+
const subtitle = feedItem.category
|
|
46
|
+
const {guideOrTutorial = {} as Guide} = feedItem
|
|
47
|
+
const href =
|
|
48
|
+
(guideOrTutorial.slug
|
|
49
|
+
? createUrl(guideOrTutorial.slug, guideOrTutorial._type)
|
|
50
|
+
: feedItem.externalLink) || feedItem.externalLink
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Flex
|
|
54
|
+
as="li"
|
|
55
|
+
key={feedItem._id}
|
|
56
|
+
paddingRight={index < feedItems?.length - 1 ? 1 : 3}
|
|
57
|
+
paddingLeft={index === 0 ? 3 : 0}
|
|
58
|
+
align="stretch"
|
|
59
|
+
style={{minWidth: 272, width: '30%'}}
|
|
60
|
+
>
|
|
61
|
+
<Tutorial
|
|
62
|
+
title={feedItem.title}
|
|
63
|
+
href={href ?? ''}
|
|
64
|
+
presenterName={presenter.name}
|
|
65
|
+
presenterSubtitle={subtitle}
|
|
66
|
+
showPlayIcon={feedItem.hasVideo}
|
|
67
|
+
posterURL={
|
|
68
|
+
feedItem.poster ? urlBuilder.image(feedItem.poster).height(360).url() : undefined
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
</Flex>
|
|
72
|
+
)
|
|
73
|
+
})}
|
|
74
|
+
</Flex>
|
|
75
|
+
</DashboardWidgetContainer>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {Card, Box, Heading, Flex, Text, Stack} from '@sanity/ui'
|
|
3
|
+
import {PlayIcon} from '@sanity/icons'
|
|
4
|
+
import styled from 'styled-components'
|
|
5
|
+
|
|
6
|
+
const PlayIconBox = styled(Box)`
|
|
7
|
+
position: absolute;
|
|
8
|
+
top: 50%;
|
|
9
|
+
left: 50%;
|
|
10
|
+
transform: translate(-50%, -50%);
|
|
11
|
+
|
|
12
|
+
&:before {
|
|
13
|
+
content: '';
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: 50%;
|
|
16
|
+
left: 50%;
|
|
17
|
+
transform: translate(-50%, -50%);
|
|
18
|
+
width: 2.75em;
|
|
19
|
+
height: 2.75em;
|
|
20
|
+
border-radius: 50%;
|
|
21
|
+
background: ${({theme}) => theme.sanity.color.card.enabled.bg};
|
|
22
|
+
opacity: 0.75;
|
|
23
|
+
}
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
const Root = styled(Flex)`
|
|
27
|
+
&:hover {
|
|
28
|
+
${PlayIconBox} {
|
|
29
|
+
&:before {
|
|
30
|
+
opacity: 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
const PosterCard = styled(Card)`
|
|
37
|
+
width: 100%;
|
|
38
|
+
padding-bottom: calc(9 / 16 * 100%);
|
|
39
|
+
position: relative;
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
const Poster = styled.img`
|
|
43
|
+
position: absolute;
|
|
44
|
+
top: 0;
|
|
45
|
+
left: 0;
|
|
46
|
+
height: 100%;
|
|
47
|
+
width: 100%;
|
|
48
|
+
object-fit: cover;
|
|
49
|
+
display: block;
|
|
50
|
+
|
|
51
|
+
&:not([src]) {
|
|
52
|
+
display: none;
|
|
53
|
+
}
|
|
54
|
+
`
|
|
55
|
+
|
|
56
|
+
export interface TutorialProps {
|
|
57
|
+
title: string
|
|
58
|
+
posterURL?: string
|
|
59
|
+
href: string
|
|
60
|
+
showPlayIcon?: boolean
|
|
61
|
+
presenterName?: string
|
|
62
|
+
presenterSubtitle?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function Tutorial(props: TutorialProps) {
|
|
66
|
+
const {title, posterURL, showPlayIcon, href, presenterName, presenterSubtitle} = props
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Root flex={1}>
|
|
70
|
+
<Card
|
|
71
|
+
sizing="border"
|
|
72
|
+
flex={1}
|
|
73
|
+
padding={2}
|
|
74
|
+
radius={2}
|
|
75
|
+
as="a"
|
|
76
|
+
href={href}
|
|
77
|
+
target="_blank"
|
|
78
|
+
rel="noopener noreferrer"
|
|
79
|
+
style={{position: 'relative'}}
|
|
80
|
+
>
|
|
81
|
+
<Flex direction="column" style={{height: '100%'}}>
|
|
82
|
+
{posterURL && (
|
|
83
|
+
<PosterCard marginBottom={1}>
|
|
84
|
+
<Poster src={posterURL} />
|
|
85
|
+
{showPlayIcon && (
|
|
86
|
+
<PlayIconBox display="flex">
|
|
87
|
+
<Text align="center">
|
|
88
|
+
<PlayIcon />
|
|
89
|
+
</Text>
|
|
90
|
+
</PlayIconBox>
|
|
91
|
+
)}
|
|
92
|
+
</PosterCard>
|
|
93
|
+
)}
|
|
94
|
+
<Flex direction="column" justify="space-between" paddingY={2} flex={1}>
|
|
95
|
+
<Heading as="h3" size={1}>
|
|
96
|
+
{title}
|
|
97
|
+
</Heading>
|
|
98
|
+
<Box marginTop={4}>
|
|
99
|
+
<Stack space={2} flex={1}>
|
|
100
|
+
<Text size={1}>{presenterName}</Text>
|
|
101
|
+
<Text size={0} style={{opacity: 0.7}}>
|
|
102
|
+
{presenterSubtitle}
|
|
103
|
+
</Text>
|
|
104
|
+
</Stack>
|
|
105
|
+
</Box>
|
|
106
|
+
</Flex>
|
|
107
|
+
</Flex>
|
|
108
|
+
</Card>
|
|
109
|
+
</Root>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {useMemo} from 'react'
|
|
2
|
+
import {useVersionedClient} from '../../versionedClient'
|
|
3
|
+
import imageUrlBuilder from '@sanity/image-url'
|
|
4
|
+
|
|
5
|
+
const tutorialsProjectConfig = {
|
|
6
|
+
projectId: '3do82whm',
|
|
7
|
+
dataset: 'next',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Guide {
|
|
11
|
+
_type?: string
|
|
12
|
+
slug?: {current: string}
|
|
13
|
+
presenter?: {
|
|
14
|
+
name?: string
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FeedItem {
|
|
19
|
+
_id: string
|
|
20
|
+
title?: string
|
|
21
|
+
poster?: string
|
|
22
|
+
category?: string
|
|
23
|
+
guideOrTutorial?: Guide
|
|
24
|
+
externalLink?: string
|
|
25
|
+
presenter?: {
|
|
26
|
+
name?: string
|
|
27
|
+
}
|
|
28
|
+
hasVideo?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useDataAdapter() {
|
|
32
|
+
const versionedClient = useVersionedClient()
|
|
33
|
+
return useMemo(
|
|
34
|
+
() => ({
|
|
35
|
+
getFeed: (templateRepoId: string) => {
|
|
36
|
+
const uri = templateRepoId
|
|
37
|
+
? `/addons/dashboard?templateRepoId=${templateRepoId}`
|
|
38
|
+
: '/addons/dashboard'
|
|
39
|
+
return versionedClient.observable.request<{items: FeedItem[]}>({
|
|
40
|
+
uri,
|
|
41
|
+
withCredentials: false,
|
|
42
|
+
})
|
|
43
|
+
},
|
|
44
|
+
urlBuilder: imageUrlBuilder(tutorialsProjectConfig),
|
|
45
|
+
}),
|
|
46
|
+
[versionedClient]
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {SanityTutorials} from './SanityTutorials'
|
|
2
|
+
import {LayoutConfig, DashboardWidget} from '../../types'
|
|
3
|
+
|
|
4
|
+
export function sanityTutorialsWidget(config?: {layout?: LayoutConfig}): DashboardWidget {
|
|
5
|
+
return {
|
|
6
|
+
name: 'sanity-tutorials',
|
|
7
|
+
component: SanityTutorials,
|
|
8
|
+
layout: config?.layout ?? {width: 'full'},
|
|
9
|
+
}
|
|
10
|
+
}
|
package/.babelrc
DELETED