rankrunners-cms 0.0.1

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 (49) hide show
  1. package/CLAUDE.md +106 -0
  2. package/README.md +15 -0
  3. package/package.json +39 -0
  4. package/src/CaptchaBadge.tsx +72 -0
  5. package/src/editor/blocks/Blank/index.tsx +15 -0
  6. package/src/editor/blocks/Blank/styles.module.css +4 -0
  7. package/src/editor/blocks/Button/index.tsx +46 -0
  8. package/src/editor/blocks/Card/index.tsx +67 -0
  9. package/src/editor/blocks/Card/styles.module.css +54 -0
  10. package/src/editor/blocks/Container/index.tsx +36 -0
  11. package/src/editor/blocks/Flex/index.tsx +82 -0
  12. package/src/editor/blocks/Flex/styles.module.css +9 -0
  13. package/src/editor/blocks/Grid/index.tsx +53 -0
  14. package/src/editor/blocks/Grid/styles.module.css +11 -0
  15. package/src/editor/blocks/Heading/index.tsx +69 -0
  16. package/src/editor/blocks/Hero/Hero.tsx +107 -0
  17. package/src/editor/blocks/Hero/client.tsx +204 -0
  18. package/src/editor/blocks/Hero/index.tsx +2 -0
  19. package/src/editor/blocks/Hero/quotes.ts +46 -0
  20. package/src/editor/blocks/Hero/server.tsx +7 -0
  21. package/src/editor/blocks/Hero/styles.module.css +116 -0
  22. package/src/editor/blocks/Logos/index.tsx +77 -0
  23. package/src/editor/blocks/Logos/styles.module.css +13 -0
  24. package/src/editor/blocks/Paragraph/index.tsx +95 -0
  25. package/src/editor/blocks/Paragraph/styles.module.css +4 -0
  26. package/src/editor/blocks/Space/index.tsx +45 -0
  27. package/src/editor/blocks/Space/styles.module.css +14 -0
  28. package/src/editor/blocks/Stats/index.tsx +58 -0
  29. package/src/editor/blocks/Stats/styles.module.css +64 -0
  30. package/src/editor/blocks/Text/index.tsx +75 -0
  31. package/src/editor/blocks/Text/styles.module.css +4 -0
  32. package/src/editor/components/Layout/index.tsx +160 -0
  33. package/src/editor/components/Layout/styles.module.css +3 -0
  34. package/src/editor/components/Section/index.tsx +31 -0
  35. package/src/editor/components/Section/styles.module.css +23 -0
  36. package/src/editor/index.tsx +63 -0
  37. package/src/editor/options.ts +37 -0
  38. package/src/editor/types.ts +60 -0
  39. package/src/editor/utils/generateId.ts +2 -0
  40. package/src/editor/utils/getClassNameFactory.ts +63 -0
  41. package/src/index.css +1 -0
  42. package/src/index.ts +6 -0
  43. package/src/libs/redirect.ts +10 -0
  44. package/src/sitemap/handleRobotsTxt.ts +19 -0
  45. package/src/sitemap/handleSitemap.ts +25 -0
  46. package/src/sitemap/index.ts +2 -0
  47. package/src/styles.d.ts +4 -0
  48. package/tsconfig.json +42 -0
  49. package/tsdown.config.ts +15 -0
@@ -0,0 +1,107 @@
1
+ import { Button } from '@measured/puck'
2
+ import { Section } from '../../components/Section'
3
+ import styles from './styles.module.css'
4
+ import type { PuckComponent, Slot } from '@measured/puck'
5
+ import type { ReactNode } from 'react'
6
+ import getClassNameFactory from '../../utils/getClassNameFactory'
7
+
8
+ const getClassName = getClassNameFactory('Hero', styles)
9
+
10
+ export type HeroProps = {
11
+ quote?: { index: number; label: string }
12
+ title: string | ReactNode
13
+ description: string
14
+ align?: string
15
+ padding: string
16
+ image?: {
17
+ content?: Slot
18
+ mode?: 'inline' | 'background' | 'custom'
19
+ url?: string
20
+ }
21
+ buttons: Array<{
22
+ label: string
23
+ href: string
24
+ variant?: 'primary' | 'secondary'
25
+ }>
26
+ }
27
+
28
+ export const Hero: PuckComponent<HeroProps> = ({
29
+ align,
30
+ title,
31
+ description,
32
+ buttons,
33
+ padding,
34
+ image,
35
+ puck,
36
+ }) => {
37
+ return (
38
+ <Section
39
+ className={getClassName({
40
+ left: align === 'left',
41
+ center: align === 'center',
42
+ hasImageBackground: image?.mode === 'background',
43
+ })}
44
+ style={{ paddingTop: padding, paddingBottom: padding }}
45
+ >
46
+ {image?.mode === 'background' && (
47
+ <>
48
+ <div
49
+ className={getClassName('image')}
50
+ style={{
51
+ backgroundImage: `url("${image.url}")`,
52
+ }}
53
+ ></div>
54
+
55
+ <div className={getClassName('imageOverlay')}></div>
56
+ </>
57
+ )}
58
+
59
+ <div className={getClassName('inner')}>
60
+ <div className={getClassName('content')}>
61
+ <h1>{title}</h1>
62
+ <p className={getClassName('subtitle')}>{description}</p>
63
+ <div className={getClassName('actions')}>
64
+ {buttons.map((button, i) => (
65
+ <Button
66
+ key={i}
67
+ href={button.href}
68
+ variant={button.variant}
69
+ size="large"
70
+ tabIndex={puck.isEditing ? -1 : undefined}
71
+ >
72
+ {button.label}
73
+ </Button>
74
+ ))}
75
+ </div>
76
+ </div>
77
+
78
+ {align !== 'center' && image?.mode === 'inline' && image.url && (
79
+ <div
80
+ style={{
81
+ backgroundImage: `url('${image.url}')`,
82
+ backgroundSize: 'cover',
83
+ backgroundRepeat: 'no-repeat',
84
+ backgroundPosition: 'center',
85
+ borderRadius: 24,
86
+ height: 356,
87
+ marginLeft: 'auto',
88
+ width: '100%',
89
+ }}
90
+ />
91
+ )}
92
+
93
+ {align !== 'center' && image?.mode === 'custom' && image.content && (
94
+ <image.content
95
+ style={{
96
+ height: 356,
97
+ marginLeft: 'auto',
98
+ width: '100%',
99
+ }}
100
+ />
101
+ )}
102
+ </div>
103
+ </Section>
104
+ )
105
+ }
106
+
107
+ export default Hero
@@ -0,0 +1,204 @@
1
+ import { Link2 } from 'lucide-react'
2
+ import { AutoField, FieldLabel } from '@measured/puck'
3
+ import { quotes } from './quotes'
4
+ import HeroComponent from './Hero'
5
+ import type { ComponentConfig } from '@measured/puck'
6
+ import type { HeroProps } from './Hero'
7
+
8
+ export const Hero: ComponentConfig<{
9
+ props: HeroProps
10
+ fields: {
11
+ userField: {
12
+ type: 'userField'
13
+ option: boolean
14
+ }
15
+ }
16
+ }> = {
17
+ fields: {
18
+ quote: {
19
+ type: 'external',
20
+ placeholder: 'Select a quote',
21
+ showSearch: false,
22
+ renderFooter: ({ items }) => {
23
+ return (
24
+ <div>
25
+ {items.length} result{items.length === 1 ? '' : 's'}
26
+ </div>
27
+ )
28
+ },
29
+ filterFields: {
30
+ author: {
31
+ type: 'select',
32
+ options: [
33
+ { value: '', label: 'Select an author' },
34
+ { value: 'Mark Twain', label: 'Mark Twain' },
35
+ { value: 'Henry Ford', label: 'Henry Ford' },
36
+ { value: 'Kurt Vonnegut', label: 'Kurt Vonnegut' },
37
+ { value: 'Andrew Carnegie', label: 'Andrew Carnegie' },
38
+ { value: 'C. S. Lewis', label: 'C. S. Lewis' },
39
+ { value: 'Confucius', label: 'Confucius' },
40
+ { value: 'Eleanor Roosevelt', label: 'Eleanor Roosevelt' },
41
+ { value: 'Samuel Ullman', label: 'Samuel Ullman' },
42
+ ],
43
+ },
44
+ },
45
+ fetchList: async ({ query, filters }) => {
46
+ // Simulate delay
47
+ await new Promise((res) => setTimeout(res, 500))
48
+
49
+ return quotes
50
+ .map((quote, idx) => ({
51
+ index: idx,
52
+ title: quote.author,
53
+ description: quote.content,
54
+ }))
55
+ .filter((item) => {
56
+ if (filters.author && item.title !== filters.author) {
57
+ return false
58
+ }
59
+
60
+ if (!query) return true
61
+
62
+ const queryLowercase = query.toLowerCase()
63
+
64
+ if (item.title.toLowerCase().indexOf(queryLowercase) > -1) {
65
+ return true
66
+ }
67
+
68
+ if (item.description.toLowerCase().indexOf(queryLowercase) > -1) {
69
+ return true
70
+ }
71
+ })
72
+ },
73
+ mapRow: (item) => ({
74
+ title: item.title,
75
+ description: <span>{item.description}</span>,
76
+ }),
77
+ mapProp: (result) => {
78
+ return { index: result.index, label: result.description }
79
+ },
80
+ getItemSummary: (item) => item.label,
81
+ },
82
+ title: { type: 'text', contentEditable: true },
83
+ description: { type: 'textarea', contentEditable: true },
84
+ buttons: {
85
+ type: 'array',
86
+ min: 1,
87
+ max: 4,
88
+ getItemSummary: (item) => item.label || 'Button',
89
+ arrayFields: {
90
+ label: { type: 'text', contentEditable: true },
91
+ href: { type: 'text' },
92
+ variant: {
93
+ type: 'select',
94
+ options: [
95
+ { label: 'primary', value: 'primary' },
96
+ { label: 'secondary', value: 'secondary' },
97
+ ],
98
+ },
99
+ },
100
+ defaultItemProps: {
101
+ label: 'Button',
102
+ href: '#',
103
+ },
104
+ },
105
+ align: {
106
+ type: 'radio',
107
+ options: [
108
+ { label: 'left', value: 'left' },
109
+ { label: 'center', value: 'center' },
110
+ ],
111
+ },
112
+ image: {
113
+ type: 'object',
114
+ objectFields: {
115
+ content: { type: 'slot' },
116
+ url: {
117
+ type: 'custom',
118
+ render: ({ value, field, name, onChange, readOnly }) => (
119
+ <FieldLabel
120
+ label={field.label || name}
121
+ readOnly={readOnly}
122
+ icon={<Link2 size="16" />}
123
+ >
124
+ <AutoField
125
+ field={{ type: 'text' }}
126
+ value={value}
127
+ onChange={onChange}
128
+ readOnly={readOnly}
129
+ />
130
+ </FieldLabel>
131
+ ),
132
+ },
133
+ mode: {
134
+ type: 'radio',
135
+ options: [
136
+ { label: 'inline', value: 'inline' },
137
+ { label: 'bg', value: 'background' },
138
+ { label: 'custom', value: 'custom' },
139
+ ],
140
+ },
141
+ },
142
+ },
143
+ padding: { type: 'userField', option: true },
144
+ },
145
+ defaultProps: {
146
+ title: 'Hero',
147
+ align: 'left',
148
+ description: 'Description',
149
+ buttons: [{ label: 'Learn more', href: '#' }],
150
+ padding: '64px',
151
+ },
152
+ /**
153
+ * The resolveData method allows us to modify component data after being
154
+ * set by the user.
155
+ *
156
+ * It is called after the page data is changed, but before a component
157
+ * is rendered. This allows us to make dynamic changes to the props
158
+ * without storing the data in Puck.
159
+ *
160
+ * For example, requesting a third-party API for the latest content.
161
+ */
162
+ resolveData: async ({ props }, { changed }) => {
163
+ if (!props.quote)
164
+ return { props, readOnly: { title: false, description: false } }
165
+
166
+ if (!changed.quote) {
167
+ return { props }
168
+ }
169
+
170
+ // Simulate a delay
171
+ await new Promise((resolve) => setTimeout(resolve, 500))
172
+
173
+ return {
174
+ props: {
175
+ title: quotes[props.quote.index]?.author,
176
+ description: quotes[props.quote.index]?.content,
177
+ },
178
+ readOnly: { title: true, description: true },
179
+ }
180
+ },
181
+ resolveFields: (data, { fields }) => {
182
+ if (data.props.align === 'center') {
183
+ return {
184
+ ...fields,
185
+ image: undefined,
186
+ }
187
+ }
188
+
189
+ return fields
190
+ },
191
+ resolvePermissions: async (data, params) => {
192
+ if (!params.changed.quote) return params.lastPermissions
193
+
194
+ // Simulate delay
195
+ await new Promise((resolve) => setTimeout(resolve, 500))
196
+
197
+ return {
198
+ ...params.permissions,
199
+ // Disable delete if quote 7 is selected
200
+ delete: data.props.quote?.index !== 7,
201
+ }
202
+ },
203
+ render: HeroComponent,
204
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./client";
2
+ export { type HeroProps } from "./Hero";
@@ -0,0 +1,46 @@
1
+ export const quotes = [
2
+ {
3
+ content:
4
+ "Age is an issue of mind over matter. If you don't mind, it doesn't matter.",
5
+ author: "Mark Twain",
6
+ },
7
+ {
8
+ content:
9
+ "Anyone who stops learning is old, whether at twenty or eighty. Anyone who keeps learning stays young. The greatest thing in life is to keep your mind young.",
10
+ author: "Henry Ford",
11
+ },
12
+ {
13
+ content: "Wrinkles should merely indicate where smiles have been.",
14
+ author: "Mark Twain",
15
+ },
16
+ {
17
+ content:
18
+ "True terror is to wake up one morning and discover that your high school class is running the country.",
19
+ author: "Kurt Vonnegut",
20
+ },
21
+ {
22
+ content:
23
+ "As I grow older, I pay less attention to what men say. I just watch what they do.",
24
+ author: "Andrew Carnegie",
25
+ },
26
+ {
27
+ content:
28
+ "How incessant and great are the ills with which a prolonged old age is replete.",
29
+ author: "C. S. Lewis",
30
+ },
31
+ {
32
+ content:
33
+ "Old age, believe me, is a good and pleasant thing. It is true you are gently shouldered off the stage, but then you are given such a comfortable front stall as spectator.",
34
+ author: "Confucius",
35
+ },
36
+ {
37
+ content:
38
+ "Old age has deformities enough of its own. It should never add to them the deformity of vice.",
39
+ author: "Eleanor Roosevelt",
40
+ },
41
+ {
42
+ content:
43
+ "Nobody grows old merely by living a number of years. We grow old by deserting our ideals. Years may wrinkle the skin, but to give up enthusiasm wrinkles the soul.",
44
+ author: "Samuel Ullman",
45
+ },
46
+ ];
@@ -0,0 +1,7 @@
1
+ import HeroComponent from './Hero'
2
+ import type { ComponentConfig } from '@measured/puck'
3
+ import type { HeroProps } from './Hero'
4
+
5
+ export const Hero: ComponentConfig<HeroProps> = {
6
+ render: HeroComponent,
7
+ }
@@ -0,0 +1,116 @@
1
+ .Hero {
2
+ background-image: linear-gradient(
3
+ rgba(255, 255, 255, 0),
4
+ rgb(247, 250, 255) 100%
5
+ );
6
+ display: flex;
7
+ align-items: center;
8
+ position: relative;
9
+ }
10
+
11
+ .Hero-inner {
12
+ display: flex;
13
+ align-items: center;
14
+ position: relative;
15
+ gap: 48px;
16
+ flex-wrap: wrap;
17
+ z-index: 1;
18
+ }
19
+
20
+ @media (min-width: 1024px) {
21
+ .Hero-inner {
22
+ flex-wrap: nowrap;
23
+ }
24
+ }
25
+
26
+ .Hero h1 {
27
+ line-height: 1.1;
28
+ font-size: 48px;
29
+ margin: 0;
30
+ }
31
+
32
+ @media (min-width: 768px) {
33
+ .Hero h1 {
34
+ font-size: 64px;
35
+ }
36
+ }
37
+
38
+ .Hero-subtitle {
39
+ color: var(--puck-color-grey-05);
40
+ font-size: var(--puck-font-size-m);
41
+ line-height: 1.5;
42
+ margin: 0;
43
+ margin-bottom: 8px;
44
+ font-weight: 300;
45
+ }
46
+
47
+ .Hero--hasImageBackground .Hero-subtitle {
48
+ color: var(--puck-color-grey-03);
49
+ }
50
+
51
+ .Hero-image {
52
+ background-repeat: no-repeat;
53
+ background-size: cover;
54
+ background-position: center;
55
+ position: absolute;
56
+ right: 0;
57
+ top: 0;
58
+ bottom: 0;
59
+ left: 0;
60
+ }
61
+
62
+ .Hero-imageOverlay {
63
+ background-image: linear-gradient(
64
+ -90deg,
65
+ rgb(247, 250, 255, 0.7) 0%,
66
+ rgb(247, 250, 255, 0.7) 80%
67
+ );
68
+ position: absolute;
69
+ right: 0;
70
+ top: 0;
71
+ bottom: 0;
72
+ left: 0;
73
+ }
74
+
75
+ @media (min-width: 768px) {
76
+ .Hero--left .Hero-imageOverlay {
77
+ background-image: linear-gradient(
78
+ -90deg,
79
+ rgba(255, 255, 255, 0) 0%,
80
+ rgb(247, 250, 255) 70%
81
+ );
82
+ }
83
+ }
84
+
85
+ .Hero-bg img {
86
+ height: 100%;
87
+ }
88
+
89
+ .Hero-content {
90
+ display: flex;
91
+ flex-direction: column;
92
+ gap: 16px;
93
+ width: 100%;
94
+ }
95
+
96
+ @media (min-width: 768px) {
97
+ .Hero--hasImageBackground .Hero-content {
98
+ max-width: 50%;
99
+ }
100
+ }
101
+
102
+ .Hero--center .Hero-inner {
103
+ justify-content: center;
104
+ text-align: center;
105
+ }
106
+
107
+ .Hero--center .Hero-content {
108
+ align-items: center;
109
+ justify-content: center;
110
+ max-width: 100%;
111
+ }
112
+
113
+ .Hero-actions {
114
+ display: flex;
115
+ gap: 16px;
116
+ }
@@ -0,0 +1,77 @@
1
+ import { Section } from '../../components/Section'
2
+ import styles from './styles.module.css'
3
+ import type { ComponentConfig } from '@measured/puck'
4
+ import getClassNameFactory from '../../utils/getClassNameFactory'
5
+
6
+ const getClassName = getClassNameFactory('Logos', styles)
7
+
8
+ export type LogosProps = {
9
+ logos: Array<{
10
+ alt: string
11
+ imageUrl: string
12
+ }>
13
+ }
14
+
15
+ export const Logos: ComponentConfig<LogosProps> = {
16
+ fields: {
17
+ logos: {
18
+ type: 'array',
19
+ getItemSummary: (item, i) => item.alt || `Feature #${i}`,
20
+ defaultItemProps: {
21
+ alt: '',
22
+ imageUrl: '',
23
+ },
24
+ arrayFields: {
25
+ alt: { type: 'text' },
26
+ imageUrl: { type: 'text' },
27
+ },
28
+ },
29
+ },
30
+ defaultProps: {
31
+ logos: [
32
+ {
33
+ alt: 'google',
34
+ imageUrl:
35
+ 'https://logolook.net/wp-content/uploads/2021/06/Google-Logo.png',
36
+ },
37
+ {
38
+ alt: 'google',
39
+ imageUrl:
40
+ 'https://logolook.net/wp-content/uploads/2021/06/Google-Logo.png',
41
+ },
42
+ {
43
+ alt: 'google',
44
+ imageUrl:
45
+ 'https://logolook.net/wp-content/uploads/2021/06/Google-Logo.png',
46
+ },
47
+ {
48
+ alt: 'google',
49
+ imageUrl:
50
+ 'https://logolook.net/wp-content/uploads/2021/06/Google-Logo.png',
51
+ },
52
+ {
53
+ alt: 'google',
54
+ imageUrl:
55
+ 'https://logolook.net/wp-content/uploads/2021/06/Google-Logo.png',
56
+ },
57
+ ],
58
+ },
59
+ render: ({ logos }) => {
60
+ return (
61
+ <Section className={getClassName()}>
62
+ <div className={getClassName('items')}>
63
+ {logos.map((item, i) => (
64
+ <div key={i} className={getClassName('item')}>
65
+ <img
66
+ className={getClassName('image')}
67
+ alt={item.alt}
68
+ src={item.imageUrl}
69
+ height={64}
70
+ ></img>
71
+ </div>
72
+ ))}
73
+ </div>
74
+ </Section>
75
+ )
76
+ },
77
+ }
@@ -0,0 +1,13 @@
1
+ .Logos {
2
+ background-color: var(--puck-color-grey-02);
3
+ }
4
+
5
+ .Logos-items {
6
+ display: flex;
7
+ justify-content: space-between;
8
+ padding-bottom: 64px;
9
+ padding-top: 64px;
10
+ gap: 24px;
11
+ filter: grayscale(1) brightness(100);
12
+ opacity: 0.8;
13
+ }
@@ -0,0 +1,95 @@
1
+ import { ALargeSmall, AlignLeft } from 'lucide-react'
2
+
3
+ import { Section } from '../../components/Section'
4
+ import { withLayout } from '../../components/Layout'
5
+ import type { ComponentConfig } from '@measured/puck'
6
+ import type { WithLayout } from '../../components/Layout'
7
+
8
+ export type ParagraphProps = WithLayout<{
9
+ align: 'left' | 'center' | 'right'
10
+ text?: string
11
+ padding?: string
12
+ size?: 's' | 'm' | 'l'
13
+ italic?: boolean
14
+ bold?: boolean
15
+ maxWidth?: string
16
+ }>
17
+
18
+ const ParagraphInner: ComponentConfig<ParagraphProps> = {
19
+ fields: {
20
+ text: {
21
+ label: 'Text',
22
+ type: 'textarea',
23
+ contentEditable: true,
24
+ },
25
+ size: {
26
+ label: 'Size',
27
+ type: 'select',
28
+ labelIcon: <ALargeSmall size={16} />,
29
+ options: [
30
+ { label: 'S', value: 's' },
31
+ { label: 'M', value: 'm' },
32
+ { label: 'L', value: 'l' },
33
+ ],
34
+ },
35
+ italic: {
36
+ label: 'Italic',
37
+ type: 'radio',
38
+ options: [
39
+ { label: 'true', value: true },
40
+ { label: 'false', value: false },
41
+ ],
42
+ },
43
+ bold: {
44
+ label: 'Bold',
45
+ type: 'radio',
46
+ options: [
47
+ { label: 'true', value: true },
48
+ { label: 'false', value: false },
49
+ ],
50
+ },
51
+ align: {
52
+ label: 'Text Alignment',
53
+ type: 'radio',
54
+ labelIcon: <AlignLeft size={16} />,
55
+ options: [
56
+ { label: 'Left', value: 'left' },
57
+ { label: 'Center', value: 'center' },
58
+ { label: 'Right', value: 'right' },
59
+ ],
60
+ },
61
+ maxWidth: { label: 'Maximum Width', type: 'text' },
62
+ },
63
+ defaultProps: {
64
+ align: 'left',
65
+ text: 'Paragraph',
66
+ size: 'm',
67
+ },
68
+ render: ({ align, text, italic, bold, size, maxWidth }) => {
69
+ return (
70
+ <Section maxWidth={maxWidth}>
71
+ <p
72
+ style={{
73
+ display: 'flex',
74
+ textAlign: align,
75
+ width: '100%',
76
+ fontStyle: italic ? 'italic' : 'normal',
77
+ fontWeight: bold ? 700 : 400,
78
+ fontSize: size === 'l' ? '28px' : size === 'm' ? '20px' : '16px',
79
+ maxWidth,
80
+ justifyContent:
81
+ align === 'center'
82
+ ? 'center'
83
+ : align === 'right'
84
+ ? 'flex-end'
85
+ : 'flex-start',
86
+ }}
87
+ >
88
+ {text}
89
+ </p>
90
+ </Section>
91
+ )
92
+ },
93
+ }
94
+
95
+ export const Paragraph = withLayout(ParagraphInner)
@@ -0,0 +1,4 @@
1
+ .Text {
2
+ line-height: 1.5;
3
+ padding: 0px;
4
+ }