@stacksjs/defaults 0.70.380 → 0.71.2
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/app/Actions/Auth/MagicLinkConsumeAction.ts +53 -0
- package/app/Actions/Auth/MagicLinkSendAction.ts +35 -0
- package/app/Actions/Cms/SitemapAction.ts +23 -2
- package/app/Jobs/PublishScheduledPagesJob.ts +26 -0
- package/app/Middleware/Site.ts +14 -0
- package/app/Middleware.ts +1 -0
- package/app/Models/Automation.ts +23 -0
- package/app/Models/AutomationRun.ts +26 -0
- package/app/Models/Campaign.ts +67 -3
- package/app/Models/CampaignSend.ts +90 -6
- package/app/Models/CampaignVariant.ts +25 -0
- package/app/Models/CommunicationSuppression.ts +22 -0
- package/app/Models/ConsentEvent.ts +26 -0
- package/app/Models/Content/Menu.ts +56 -0
- package/app/Models/Content/MenuItem.ts +91 -0
- package/app/Models/Content/Page.ts +111 -14
- package/app/Models/Content/PageRevision.ts +86 -0
- package/app/Models/Content/Post.ts +24 -9
- package/app/Models/Content/Redirect.ts +80 -0
- package/app/Models/Forms/Form.ts +91 -0
- package/app/Models/Forms/FormField.ts +127 -0
- package/app/Models/Forms/FormSubmission.ts +114 -0
- package/app/Models/MagicLinkToken.ts +97 -0
- package/app/Models/SenderDomain.ts +22 -0
- package/app/Models/Site.ts +112 -0
- package/app/Models/SiteDomain.ts +74 -0
- package/app/Models/SmsOptOut.ts +65 -0
- package/app/Models/UsageEvent.ts +24 -0
- package/app/Models/commerce/Auction.ts +173 -0
- package/app/Models/commerce/AuctionItem.ts +204 -0
- package/app/Models/commerce/Bid.ts +129 -0
- package/app/Models/commerce/Pledge.ts +112 -0
- package/bootstrap.ts +7 -0
- package/functions/public-application-url.ts +1 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/functions/dashboard/sidebar.ts +109 -3
- package/resources/functions/dashboard/toggles.ts +90 -2
- package/resources/views/cms/blocks/columns.stx +10 -0
- package/resources/views/cms/blocks/cta.stx +8 -0
- package/resources/views/cms/blocks/embed.stx +14 -0
- package/resources/views/cms/blocks/form.stx +132 -0
- package/resources/views/cms/blocks/hero.stx +16 -0
- package/resources/views/cms/blocks/image.stx +7 -0
- package/resources/views/cms/blocks/rich-text.stx +4 -0
- package/resources/views/cms/page.stx +28 -0
- package/routes/auth.ts +7 -0
- package/routes/forms.ts +110 -0
- package/views/auth/magic/[token].stx +88 -0
- package/views/dashboard/.discovered-models.json +46 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An auction: a catalogue of lots that opens, takes bids, and closes.
|
|
6
|
+
*
|
|
7
|
+
* `eventId` is a plain column rather than a `belongsTo`, because the thing an
|
|
8
|
+
* auction hangs off is the application's own event model - a gala, a fun run,
|
|
9
|
+
* a capital campaign evening - and the framework does not ship one. An app that
|
|
10
|
+
* has an Event model declares `hasOne: ['Auction']` on its side and the column
|
|
11
|
+
* below is what that resolves through; an app with no events leaves it null and
|
|
12
|
+
* runs the auction standalone.
|
|
13
|
+
*/
|
|
14
|
+
export default defineModel({
|
|
15
|
+
name: 'Auction',
|
|
16
|
+
table: 'auctions',
|
|
17
|
+
primaryKey: 'id',
|
|
18
|
+
autoIncrement: true,
|
|
19
|
+
|
|
20
|
+
traits: {
|
|
21
|
+
useUuid: true,
|
|
22
|
+
useTimestamps: true,
|
|
23
|
+
|
|
24
|
+
useSearch: {
|
|
25
|
+
displayable: ['id', 'title', 'status', 'opensAt', 'closesAt', 'goalAmount'],
|
|
26
|
+
searchable: ['title', 'description'],
|
|
27
|
+
sortable: ['createdAt', 'opensAt', 'closesAt'],
|
|
28
|
+
filterable: ['status'],
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
useApi: {
|
|
32
|
+
uri: 'auctions',
|
|
33
|
+
middleware: { read: ['auth'], write: ['auth'] },
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
observe: true,
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
hasMany: ['AuctionItem', 'Bid', 'Pledge'],
|
|
40
|
+
|
|
41
|
+
indexes: [
|
|
42
|
+
{ name: 'auctions_event_id_index', columns: ['event_id'] },
|
|
43
|
+
{ name: 'auctions_status_index', columns: ['status'] },
|
|
44
|
+
],
|
|
45
|
+
|
|
46
|
+
attributes: {
|
|
47
|
+
eventId: {
|
|
48
|
+
order: 1,
|
|
49
|
+
fillable: true,
|
|
50
|
+
validation: {
|
|
51
|
+
rule: schema.number(),
|
|
52
|
+
},
|
|
53
|
+
factory: faker => faker.number.int({ min: 1, max: 20 }),
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
title: {
|
|
57
|
+
order: 2,
|
|
58
|
+
fillable: true,
|
|
59
|
+
validation: {
|
|
60
|
+
rule: schema.string().required().max(160),
|
|
61
|
+
message: {
|
|
62
|
+
required: 'Title is required',
|
|
63
|
+
max: 'Title must have a maximum of 160 characters',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
factory: () => 'Spring Benefit Auction',
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
description: {
|
|
70
|
+
order: 3,
|
|
71
|
+
fillable: true,
|
|
72
|
+
validation: {
|
|
73
|
+
rule: schema.string(),
|
|
74
|
+
},
|
|
75
|
+
factory: faker => faker.lorem.sentence(),
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
status: {
|
|
79
|
+
default: 'draft',
|
|
80
|
+
order: 4,
|
|
81
|
+
fillable: true,
|
|
82
|
+
validation: {
|
|
83
|
+
rule: schema.enum(['draft', 'preview', 'open', 'closed', 'settled']).required(),
|
|
84
|
+
message: {
|
|
85
|
+
required: 'Status is required',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
factory: () => 'draft',
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
currency: {
|
|
92
|
+
default: 'USD',
|
|
93
|
+
order: 5,
|
|
94
|
+
fillable: true,
|
|
95
|
+
validation: {
|
|
96
|
+
rule: schema.string().max(3),
|
|
97
|
+
},
|
|
98
|
+
factory: () => 'USD',
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The fundraising target, in cents. Null when the school would rather not
|
|
103
|
+
* put a number on the wall.
|
|
104
|
+
*/
|
|
105
|
+
goalAmount: {
|
|
106
|
+
order: 6,
|
|
107
|
+
fillable: true,
|
|
108
|
+
validation: {
|
|
109
|
+
rule: schema.number().min(0),
|
|
110
|
+
},
|
|
111
|
+
factory: faker => faker.number.int({ min: 500000, max: 5000000 }),
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
opensAt: {
|
|
115
|
+
order: 7,
|
|
116
|
+
fillable: true,
|
|
117
|
+
validation: {
|
|
118
|
+
rule: schema.date().required(),
|
|
119
|
+
},
|
|
120
|
+
factory: faker => faker.date.soon().toISOString(),
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
closesAt: {
|
|
124
|
+
order: 8,
|
|
125
|
+
fillable: true,
|
|
126
|
+
validation: {
|
|
127
|
+
rule: schema.date().required(),
|
|
128
|
+
},
|
|
129
|
+
factory: faker => faker.date.future().toISOString(),
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/** How far a lot's close is pushed out when a bid lands in the window. */
|
|
133
|
+
antiSnipeMinutes: {
|
|
134
|
+
default: 2,
|
|
135
|
+
order: 9,
|
|
136
|
+
fillable: true,
|
|
137
|
+
validation: {
|
|
138
|
+
rule: schema.number().min(0).max(60),
|
|
139
|
+
},
|
|
140
|
+
factory: () => 2,
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/** How close to the end a bid has to land to trigger an extension. */
|
|
144
|
+
extendOnBidWindowMinutes: {
|
|
145
|
+
default: 2,
|
|
146
|
+
order: 10,
|
|
147
|
+
fillable: true,
|
|
148
|
+
validation: {
|
|
149
|
+
rule: schema.number().min(0).max(60),
|
|
150
|
+
},
|
|
151
|
+
factory: () => 2,
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The hard stop. Without it two determined bidders can hold one lot open
|
|
156
|
+
* all night and the gala staff cannot go home.
|
|
157
|
+
*/
|
|
158
|
+
maxExtensions: {
|
|
159
|
+
default: 20,
|
|
160
|
+
order: 11,
|
|
161
|
+
fillable: true,
|
|
162
|
+
validation: {
|
|
163
|
+
rule: schema.number().min(0),
|
|
164
|
+
},
|
|
165
|
+
factory: () => 20,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
dashboard: {
|
|
170
|
+
icon: 'i-hugeicons-auction',
|
|
171
|
+
label: 'Auctions',
|
|
172
|
+
},
|
|
173
|
+
} as const)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One lot in an auction.
|
|
6
|
+
*
|
|
7
|
+
* Every money column is integer cents. A benefit auction adds several hundred
|
|
8
|
+
* bids together and reports the total to a board; floats lose that argument.
|
|
9
|
+
*/
|
|
10
|
+
export default defineModel({
|
|
11
|
+
name: 'AuctionItem',
|
|
12
|
+
table: 'auction_items',
|
|
13
|
+
primaryKey: 'id',
|
|
14
|
+
autoIncrement: true,
|
|
15
|
+
|
|
16
|
+
traits: {
|
|
17
|
+
useUuid: true,
|
|
18
|
+
useTimestamps: true,
|
|
19
|
+
|
|
20
|
+
useSearch: {
|
|
21
|
+
displayable: ['id', 'lotNumber', 'title', 'status', 'startingBid', 'donorName'],
|
|
22
|
+
searchable: ['title', 'description', 'donorName', 'category'],
|
|
23
|
+
sortable: ['lotNumber', 'startingBid', 'closesAt'],
|
|
24
|
+
filterable: ['status', 'category'],
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
useApi: {
|
|
28
|
+
uri: 'auction-items',
|
|
29
|
+
middleware: { read: ['auth'], write: ['auth'] },
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
observe: true,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
belongsTo: ['Auction'],
|
|
36
|
+
hasMany: ['Bid'],
|
|
37
|
+
|
|
38
|
+
indexes: [
|
|
39
|
+
{ name: 'auction_items_auction_id_index', columns: ['auction_id'] },
|
|
40
|
+
{ name: 'auction_items_status_index', columns: ['status'] },
|
|
41
|
+
],
|
|
42
|
+
|
|
43
|
+
attributes: {
|
|
44
|
+
lotNumber: {
|
|
45
|
+
order: 1,
|
|
46
|
+
fillable: true,
|
|
47
|
+
validation: {
|
|
48
|
+
rule: schema.number().required().min(1),
|
|
49
|
+
message: {
|
|
50
|
+
required: 'Lot number is required',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
factory: faker => faker.number.int({ min: 1, max: 120 }),
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
title: {
|
|
57
|
+
order: 2,
|
|
58
|
+
fillable: true,
|
|
59
|
+
validation: {
|
|
60
|
+
rule: schema.string().required().max(160),
|
|
61
|
+
message: {
|
|
62
|
+
required: 'Title is required',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
factory: faker => faker.commerce.productName(),
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
description: {
|
|
69
|
+
order: 3,
|
|
70
|
+
fillable: true,
|
|
71
|
+
validation: {
|
|
72
|
+
rule: schema.string(),
|
|
73
|
+
},
|
|
74
|
+
factory: faker => faker.lorem.paragraph(),
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
imageUrl: {
|
|
78
|
+
order: 4,
|
|
79
|
+
fillable: true,
|
|
80
|
+
validation: {
|
|
81
|
+
rule: schema.string().max(500),
|
|
82
|
+
},
|
|
83
|
+
factory: () => '',
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
category: {
|
|
87
|
+
order: 5,
|
|
88
|
+
fillable: true,
|
|
89
|
+
validation: {
|
|
90
|
+
rule: schema.string().max(60),
|
|
91
|
+
},
|
|
92
|
+
factory: faker => faker.helpers.arrayElement(['experiences', 'travel', 'dining', 'sports', 'art', 'services']),
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/** Who donated the lot. It goes on the tax letter and the thank-you note. */
|
|
96
|
+
donorName: {
|
|
97
|
+
order: 6,
|
|
98
|
+
fillable: true,
|
|
99
|
+
validation: {
|
|
100
|
+
rule: schema.string().max(160),
|
|
101
|
+
},
|
|
102
|
+
factory: faker => faker.company.name(),
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* What the lot is worth, in cents. Drives the sell-through report and the
|
|
107
|
+
* "paid over value" number the development office cares about.
|
|
108
|
+
*/
|
|
109
|
+
fairMarketValue: {
|
|
110
|
+
order: 7,
|
|
111
|
+
fillable: true,
|
|
112
|
+
validation: {
|
|
113
|
+
rule: schema.number().min(0),
|
|
114
|
+
},
|
|
115
|
+
factory: faker => faker.number.int({ min: 5000, max: 200000 }),
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
startingBid: {
|
|
119
|
+
order: 8,
|
|
120
|
+
fillable: true,
|
|
121
|
+
validation: {
|
|
122
|
+
rule: schema.number().required().min(0),
|
|
123
|
+
message: {
|
|
124
|
+
required: 'Starting bid is required',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
factory: faker => faker.number.int({ min: 2500, max: 50000 }),
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A fixed increment for this lot, in cents. Null - the normal case - means
|
|
132
|
+
* the auction-wide ladder applies, which steps bigger as the money gets
|
|
133
|
+
* bigger.
|
|
134
|
+
*/
|
|
135
|
+
minIncrement: {
|
|
136
|
+
order: 9,
|
|
137
|
+
fillable: true,
|
|
138
|
+
validation: {
|
|
139
|
+
rule: schema.number().min(0),
|
|
140
|
+
},
|
|
141
|
+
factory: () => null,
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/** Bid at or above this and the lot sells immediately. */
|
|
145
|
+
buyNowPrice: {
|
|
146
|
+
order: 10,
|
|
147
|
+
fillable: true,
|
|
148
|
+
validation: {
|
|
149
|
+
rule: schema.number().min(0),
|
|
150
|
+
},
|
|
151
|
+
factory: () => null,
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/** Below this the lot passes rather than selling. */
|
|
155
|
+
reservePrice: {
|
|
156
|
+
order: 11,
|
|
157
|
+
fillable: true,
|
|
158
|
+
validation: {
|
|
159
|
+
rule: schema.number().min(0),
|
|
160
|
+
},
|
|
161
|
+
factory: () => null,
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
status: {
|
|
165
|
+
default: 'open',
|
|
166
|
+
order: 12,
|
|
167
|
+
fillable: true,
|
|
168
|
+
validation: {
|
|
169
|
+
rule: schema.enum(['open', 'closed', 'sold', 'passed']).required(),
|
|
170
|
+
},
|
|
171
|
+
factory: () => 'open',
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A per-lot close, so a gala can stagger its sections and the room is not
|
|
176
|
+
* asked to watch a hundred lots end at once. Null falls back to the
|
|
177
|
+
* auction's own close. Anti-snipe extensions are written here.
|
|
178
|
+
*/
|
|
179
|
+
closesAt: {
|
|
180
|
+
order: 13,
|
|
181
|
+
fillable: true,
|
|
182
|
+
validation: {
|
|
183
|
+
rule: schema.date(),
|
|
184
|
+
},
|
|
185
|
+
factory: () => null,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/** How many times anti-snipe has already extended this lot. */
|
|
189
|
+
extensionCount: {
|
|
190
|
+
default: 0,
|
|
191
|
+
order: 14,
|
|
192
|
+
fillable: false,
|
|
193
|
+
validation: {
|
|
194
|
+
rule: schema.number().min(0),
|
|
195
|
+
},
|
|
196
|
+
factory: () => 0,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
dashboard: {
|
|
201
|
+
icon: 'i-hugeicons-package',
|
|
202
|
+
label: 'Lots',
|
|
203
|
+
},
|
|
204
|
+
} as const)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One bid on one lot.
|
|
6
|
+
*
|
|
7
|
+
* Bidders are identified by name and email rather than by a user account: a
|
|
8
|
+
* benefit auction is bid on by grandparents on borrowed phones, and requiring
|
|
9
|
+
* a signup between them and their paddle costs the school real money.
|
|
10
|
+
*
|
|
11
|
+
* `maxAmount` is the private proxy ceiling. It must never be selected into a
|
|
12
|
+
* template or an API response - the whole mechanism depends on nobody being
|
|
13
|
+
* able to see how far the other side is willing to go. `hidden` keeps it out of
|
|
14
|
+
* the generated CRUD responses; the queries in `@stacksjs/auctions` that feed
|
|
15
|
+
* the public catalogue select columns explicitly for the same reason.
|
|
16
|
+
*/
|
|
17
|
+
export default defineModel({
|
|
18
|
+
name: 'Bid',
|
|
19
|
+
table: 'bids',
|
|
20
|
+
primaryKey: 'id',
|
|
21
|
+
autoIncrement: true,
|
|
22
|
+
|
|
23
|
+
traits: {
|
|
24
|
+
useUuid: true,
|
|
25
|
+
useTimestamps: true,
|
|
26
|
+
|
|
27
|
+
useSearch: {
|
|
28
|
+
displayable: ['id', 'bidderName', 'amount', 'status', 'placedAt'],
|
|
29
|
+
searchable: ['bidderName', 'bidderEmail'],
|
|
30
|
+
sortable: ['placedAt', 'amount'],
|
|
31
|
+
filterable: ['status'],
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Read is authenticated: the bid history of a lot is staff information
|
|
35
|
+
// while the auction is live. Writes never come through generated CRUD -
|
|
36
|
+
// a bid has to go through the engine, so the app owns that route.
|
|
37
|
+
useApi: {
|
|
38
|
+
uri: 'bids',
|
|
39
|
+
routes: ['index', 'show'],
|
|
40
|
+
middleware: ['auth'],
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
observe: true,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
belongsTo: ['Auction', 'AuctionItem'],
|
|
47
|
+
|
|
48
|
+
indexes: [
|
|
49
|
+
{ name: 'bids_auction_item_id_index', columns: ['auction_item_id'] },
|
|
50
|
+
{ name: 'bids_auction_id_index', columns: ['auction_id'] },
|
|
51
|
+
{ name: 'bids_bidder_email_index', columns: ['bidder_email'] },
|
|
52
|
+
{ name: 'bids_status_index', columns: ['status'] },
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
attributes: {
|
|
56
|
+
bidderName: {
|
|
57
|
+
order: 1,
|
|
58
|
+
fillable: true,
|
|
59
|
+
validation: {
|
|
60
|
+
rule: schema.string().required().max(160),
|
|
61
|
+
message: {
|
|
62
|
+
required: 'Your name is required',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
factory: faker => faker.person.fullName(),
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
bidderEmail: {
|
|
69
|
+
order: 2,
|
|
70
|
+
fillable: true,
|
|
71
|
+
validation: {
|
|
72
|
+
rule: schema.string().email().required(),
|
|
73
|
+
message: {
|
|
74
|
+
required: 'Your email is required',
|
|
75
|
+
email: 'That does not look like an email address',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
factory: faker => faker.internet.email().toLowerCase(),
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/** The public number: what this bidder is currently committed to, in cents. */
|
|
82
|
+
amount: {
|
|
83
|
+
order: 3,
|
|
84
|
+
fillable: true,
|
|
85
|
+
validation: {
|
|
86
|
+
rule: schema.number().required().min(1),
|
|
87
|
+
message: {
|
|
88
|
+
required: 'A bid amount is required',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
factory: faker => faker.number.int({ min: 5000, max: 200000 }),
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/** The private ceiling, in cents. Null means "this amount only". */
|
|
95
|
+
maxAmount: {
|
|
96
|
+
order: 4,
|
|
97
|
+
fillable: true,
|
|
98
|
+
hidden: true,
|
|
99
|
+
validation: {
|
|
100
|
+
rule: schema.number().min(1),
|
|
101
|
+
},
|
|
102
|
+
factory: () => null,
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
status: {
|
|
106
|
+
default: 'leading',
|
|
107
|
+
order: 5,
|
|
108
|
+
fillable: true,
|
|
109
|
+
validation: {
|
|
110
|
+
rule: schema.enum(['leading', 'outbid', 'won', 'lost', 'invalid']).required(),
|
|
111
|
+
},
|
|
112
|
+
factory: () => 'outbid',
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
placedAt: {
|
|
116
|
+
order: 6,
|
|
117
|
+
fillable: true,
|
|
118
|
+
validation: {
|
|
119
|
+
rule: schema.date().required(),
|
|
120
|
+
},
|
|
121
|
+
factory: faker => faker.date.recent().toISOString(),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
dashboard: {
|
|
126
|
+
icon: 'i-hugeicons-hand-pointing-right-01',
|
|
127
|
+
label: 'Bids',
|
|
128
|
+
},
|
|
129
|
+
} as const)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { defineModel } from '@stacksjs/orm'
|
|
2
|
+
import { schema } from '@stacksjs/validation'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A fund-a-need pledge.
|
|
6
|
+
*
|
|
7
|
+
* Fund-a-need is the part of a benefit auction that is not an auction: the room
|
|
8
|
+
* is asked to give at fixed levels and nobody competes for anything. It shares
|
|
9
|
+
* the auction only for the tally board, which is why a pledge has an amount and
|
|
10
|
+
* a level but no lot, no increment and no winner.
|
|
11
|
+
*/
|
|
12
|
+
export default defineModel({
|
|
13
|
+
name: 'Pledge',
|
|
14
|
+
table: 'pledges',
|
|
15
|
+
primaryKey: 'id',
|
|
16
|
+
autoIncrement: true,
|
|
17
|
+
|
|
18
|
+
traits: {
|
|
19
|
+
useUuid: true,
|
|
20
|
+
useTimestamps: true,
|
|
21
|
+
|
|
22
|
+
useSearch: {
|
|
23
|
+
displayable: ['id', 'donorName', 'amount', 'level', 'status'],
|
|
24
|
+
searchable: ['donorName', 'donorEmail', 'level'],
|
|
25
|
+
sortable: ['createdAt', 'amount'],
|
|
26
|
+
filterable: ['status', 'level'],
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
useApi: {
|
|
30
|
+
uri: 'pledges',
|
|
31
|
+
middleware: ['auth'],
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
observe: true,
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
belongsTo: ['Auction'],
|
|
38
|
+
|
|
39
|
+
indexes: [
|
|
40
|
+
{ name: 'pledges_auction_id_index', columns: ['auction_id'] },
|
|
41
|
+
{ name: 'pledges_status_index', columns: ['status'] },
|
|
42
|
+
],
|
|
43
|
+
|
|
44
|
+
attributes: {
|
|
45
|
+
donorName: {
|
|
46
|
+
order: 1,
|
|
47
|
+
fillable: true,
|
|
48
|
+
validation: {
|
|
49
|
+
rule: schema.string().required().max(160),
|
|
50
|
+
message: {
|
|
51
|
+
required: 'Donor name is required',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
factory: faker => faker.person.fullName(),
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
donorEmail: {
|
|
58
|
+
order: 2,
|
|
59
|
+
fillable: true,
|
|
60
|
+
validation: {
|
|
61
|
+
rule: schema.string().email().required(),
|
|
62
|
+
message: {
|
|
63
|
+
required: 'Donor email is required',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
factory: faker => faker.internet.email().toLowerCase(),
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/** The gift, in cents. */
|
|
70
|
+
amount: {
|
|
71
|
+
order: 3,
|
|
72
|
+
fillable: true,
|
|
73
|
+
validation: {
|
|
74
|
+
rule: schema.number().required().min(1),
|
|
75
|
+
message: {
|
|
76
|
+
required: 'A pledge amount is required',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
factory: faker => faker.helpers.arrayElement([25000, 50000, 100000, 250000, 500000]),
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/** The paddle-raise tier this came from, for the tally board. */
|
|
83
|
+
level: {
|
|
84
|
+
order: 4,
|
|
85
|
+
fillable: true,
|
|
86
|
+
validation: {
|
|
87
|
+
rule: schema.string().max(40),
|
|
88
|
+
},
|
|
89
|
+
factory: faker => faker.helpers.arrayElement(['250', '500', '1000', '2500', '5000']),
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Pledges made from the room are good the moment a paddle goes up; pledges
|
|
94
|
+
* taken online are often confirmed by staff afterwards. Only `confirmed`
|
|
95
|
+
* money counts toward a total anybody reports.
|
|
96
|
+
*/
|
|
97
|
+
status: {
|
|
98
|
+
default: 'confirmed',
|
|
99
|
+
order: 5,
|
|
100
|
+
fillable: true,
|
|
101
|
+
validation: {
|
|
102
|
+
rule: schema.enum(['pending', 'confirmed', 'cancelled']).required(),
|
|
103
|
+
},
|
|
104
|
+
factory: () => 'confirmed',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
dashboard: {
|
|
109
|
+
icon: 'i-hugeicons-give-blood',
|
|
110
|
+
label: 'Pledges',
|
|
111
|
+
},
|
|
112
|
+
} as const)
|
package/bootstrap.ts
CHANGED
|
@@ -148,6 +148,13 @@ if (mounts('email', feature('email'))) {
|
|
|
148
148
|
await route.register(frameworkPath('defaults/routes/email.ts'))
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// Form-builder public endpoints (`@stacksjs/forms`). Gated on the `forms`
|
|
152
|
+
// feature, which defaults OFF - an app opts in with config/forms.ts
|
|
153
|
+
// (`buddy forms:install`) and only then do the public submit routes exist.
|
|
154
|
+
if (mounts('forms', feature('forms'))) {
|
|
155
|
+
await route.register(frameworkPath('defaults/routes/forms.ts'))
|
|
156
|
+
}
|
|
157
|
+
|
|
151
158
|
// Social sign-in: `/auth/{provider}` + `/auth/{provider}/callback`
|
|
152
159
|
// (stacksjs/stacks#2276). An opt-in bundle, NOT part of the implicit default
|
|
153
160
|
// set or `all` — OAuth callback URLs in an app that configured no provider
|
|
@@ -12,7 +12,7 @@ function withProtocol(baseUrl: string): string {
|
|
|
12
12
|
if (/^https?:\/\//i.test(baseUrl))
|
|
13
13
|
return baseUrl
|
|
14
14
|
|
|
15
|
-
const host = baseUrl.split('/')[0]
|
|
15
|
+
const host = baseUrl.split('/')[0]?.split(':')[0]?.toLowerCase() ?? ''
|
|
16
16
|
const protocol = ['localhost', '127.0.0.1', '0.0.0.0', '[::1]'].includes(host)
|
|
17
17
|
? 'http'
|
|
18
18
|
: 'https'
|
package/ide/vscode/package.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.71.2",
|
|
6
6
|
"description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"license": "MIT",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@iconify-json/f7": "^1.2.2",
|
|
48
48
|
"@iconify-json/hugeicons": "^1.2.27",
|
|
49
|
-
"@stacksjs/mobile": "^0.
|
|
49
|
+
"@stacksjs/mobile": "^0.71.2",
|
|
50
50
|
"@stacksjs/sanitizer": "^0.2.113"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|