Skip to content

Commit 8cfe490

Browse files
committed
feat: subscribe to release notes
1 parent c8de767 commit 8cfe490

File tree

7 files changed

+136
-14
lines changed

7 files changed

+136
-14
lines changed

admin/app/controllers/system_controller.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { DockerService } from '#services/docker_service';
22
import { SystemService } from '#services/system_service'
33
import { SystemUpdateService } from '#services/system_update_service'
4-
import { affectServiceValidator, installServiceValidator } from '#validators/system';
4+
import { affectServiceValidator, installServiceValidator, subscribeToReleaseNotesValidator } from '#validators/system';
55
import { inject } from '@adonisjs/core'
66
import type { HttpContext } from '@adonisjs/core/http'
77

@@ -102,4 +102,10 @@ export default class SystemController {
102102
const logs = this.systemUpdateService.getUpdateLogs();
103103
response.send({ logs });
104104
}
105+
106+
107+
async subscribeToReleaseNotes({ request }: HttpContext) {
108+
const reqData = await request.validateUsing(subscribeToReleaseNotesValidator);
109+
return await this.systemService.subscribeToReleaseNotes(reqData.email);
110+
}
105111
}

admin/app/services/system_service.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,34 @@ export class SystemService {
226226
}
227227
}
228228

229+
async subscribeToReleaseNotes(email: string): Promise<{ success: boolean; message: string }> {
230+
try {
231+
const response = await axios.post(
232+
'https://api.projectnomad.us/api/v1/lists/release-notes/subscribe',
233+
{ email },
234+
{ timeout: 5000 }
235+
)
236+
237+
if (response.status === 200) {
238+
return {
239+
success: true,
240+
message: 'Successfully subscribed to release notes',
241+
}
242+
}
243+
244+
return {
245+
success: false,
246+
message: `Failed to subscribe: ${response.statusText}`,
247+
}
248+
} catch (error) {
249+
logger.error('Error subscribing to release notes:', error)
250+
return {
251+
success: false,
252+
message: `Failed to subscribe: ${error instanceof Error ? error.message : error}`,
253+
}
254+
}
255+
}
256+
229257
/**
230258
* Checks the current state of Docker containers against the database records and updates the database accordingly.
231259
* It will mark services as not installed if their corresponding containers do not exist, regardless of their running state.
@@ -241,7 +269,7 @@ export class SystemService {
241269
const containerExists = serviceStatusList.find(
242270
(s) => s.service_name === service.service_name
243271
)
244-
272+
245273
if (service.installed) {
246274
// If marked as installed but container doesn't exist, mark as not installed
247275
if (!containerExists) {

admin/app/validators/system.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import vine from '@vinejs/vine'
22

3-
export const installServiceValidator = vine.compile(vine.object({
4-
service_name: vine.string().trim()
5-
}));
3+
export const installServiceValidator = vine.compile(
4+
vine.object({
5+
service_name: vine.string().trim(),
6+
})
7+
)
68

7-
export const affectServiceValidator = vine.compile(vine.object({
9+
export const affectServiceValidator = vine.compile(
10+
vine.object({
811
service_name: vine.string().trim(),
9-
action: vine.enum(['start', 'stop', 'restart'])
10-
}));
12+
action: vine.enum(['start', 'stop', 'restart']),
13+
})
14+
)
15+
16+
export const subscribeToReleaseNotesValidator = vine.compile(
17+
vine.object({
18+
email: vine.string().email().trim(),
19+
})
20+
)

admin/inertia/components/inputs/Input.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
77
className?: string;
88
labelClassName?: string;
99
inputClassName?: string;
10+
containerClassName?: string;
1011
leftIcon?: React.ReactNode;
1112
error?: boolean;
1213
required?: boolean;
@@ -18,6 +19,7 @@ const Input: React.FC<InputProps> = ({
1819
name,
1920
labelClassName,
2021
inputClassName,
22+
containerClassName,
2123
leftIcon,
2224
error,
2325
required,
@@ -31,7 +33,7 @@ const Input: React.FC<InputProps> = ({
3133
>
3234
{label}{required ? "*" : ""}
3335
</label>
34-
<div className="mt-1.5">
36+
<div className={classNames("mt-1.5", containerClassName)}>
3537
<div className="relative">
3638
{leftIcon && (
3739
<div className="absolute left-3 top-1/2 transform -translate-y-1/2">

admin/inertia/lib/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@ class API {
285285
return response.data
286286
})()
287287
}
288+
289+
async subscribeToReleaseNotes(email: string) {
290+
return catchInternal(async () => {
291+
const response = await this.client.post<{ success: boolean; message: string }>(
292+
'/system/subscribe-release-notes',
293+
{ email }
294+
)
295+
return response.data
296+
})()
297+
}
288298
}
289299

290300
export default new API()

admin/inertia/pages/settings/update.tsx

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { useEffect, useState } from 'react'
1010
import { IconCircleCheck } from '@tabler/icons-react'
1111
import { SystemUpdateStatus } from '../../../types/system'
1212
import api from '~/lib/api'
13+
import Input from '~/components/inputs/Input'
14+
import { useMutation } from '@tanstack/react-query'
15+
import { useNotifications } from '~/context/NotificationContext'
1316

1417
export default function SystemUpdatePage(props: {
1518
system: {
@@ -18,11 +21,14 @@ export default function SystemUpdatePage(props: {
1821
currentVersion: string
1922
}
2023
}) {
24+
const { addNotification } = useNotifications()
25+
2126
const [isUpdating, setIsUpdating] = useState(false)
2227
const [updateStatus, setUpdateStatus] = useState<SystemUpdateStatus | null>(null)
2328
const [error, setError] = useState<string | null>(null)
2429
const [showLogs, setShowLogs] = useState(false)
2530
const [logs, setLogs] = useState<string>('')
31+
const [email, setEmail] = useState('')
2632

2733
useEffect(() => {
2834
if (!isUpdating) return
@@ -116,10 +122,33 @@ export default function SystemUpdatePage(props: {
116122
if (updateStatus?.stage === 'error')
117123
return <IconAlertCircle className="h-12 w-12 text-desert-red" />
118124
if (isUpdating) return <IconRefresh className="h-12 w-12 text-desert-green animate-spin" />
119-
if (props.system.updateAvailable) return <IconArrowBigUpLines className="h-16 w-16 text-desert-green" />
125+
if (props.system.updateAvailable)
126+
return <IconArrowBigUpLines className="h-16 w-16 text-desert-green" />
120127
return <IconCircleCheck className="h-16 w-16 text-desert-olive" />
121128
}
122129

130+
const subscribeToReleaseNotesMutation = useMutation({
131+
mutationKey: ['subscribeToReleaseNotes'],
132+
mutationFn: (email: string) => api.subscribeToReleaseNotes(email),
133+
onSuccess: (data) => {
134+
if (data && data.success) {
135+
addNotification({ type: 'success', message: 'Successfully subscribed to release notes!' })
136+
setEmail('')
137+
} else {
138+
addNotification({
139+
type: 'error',
140+
message: `Failed to subscribe: ${data?.message || 'Unknown error'}`,
141+
})
142+
}
143+
},
144+
onError: (error: any) => {
145+
addNotification({
146+
type: 'error',
147+
message: `Error subscribing to release notes: ${error.message || 'Unknown error'}`,
148+
})
149+
},
150+
})
151+
123152
return (
124153
<SettingsLayout>
125154
<Head title="System Update" />
@@ -128,7 +157,8 @@ export default function SystemUpdatePage(props: {
128157
<div className="mb-8">
129158
<h1 className="text-4xl font-bold text-desert-green mb-2">System Update</h1>
130159
<p className="text-desert-stone-dark">
131-
Keep your Project N.O.M.A.D. instance up to date with the latest features and improvements.
160+
Keep your Project N.O.M.A.D. instance up to date with the latest features and
161+
improvements.
132162
</p>
133163
</div>
134164

@@ -161,9 +191,7 @@ export default function SystemUpdatePage(props: {
161191
{!isUpdating && (
162192
<>
163193
<h2 className="text-2xl font-bold text-desert-green mb-2">
164-
{props.system.updateAvailable
165-
? 'Update Available'
166-
: 'System Up to Date'}
194+
{props.system.updateAvailable ? 'Update Available' : 'System Up to Date'}
167195
</h2>
168196
<p className="text-desert-stone-dark mb-6">
169197
{props.system.updateAvailable
@@ -305,6 +333,43 @@ export default function SystemUpdatePage(props: {
305333
)}
306334
</div>
307335
</div>
336+
<div className="bg-white rounded-lg border shadow-md overflow-hidden py-6 mt-6">
337+
<div className="flex flex-col md:flex-row justify-between items-center p-8 gap-y-8 md:gap-y-0 gap-x-8">
338+
<div>
339+
<h2 className="max-w-xl text-lg font-bold text-desert-green sm:text-xl lg:col-span-7">
340+
Want to stay updated with the latest from Project N.O.M.A.D.? Subscribe to receive
341+
release notes directly to your inbox. Unsubscribe anytime.
342+
</h2>
343+
</div>
344+
<div className="flex flex-col">
345+
<div className="flex gap-x-3">
346+
<Input
347+
name="email"
348+
label=""
349+
type="email"
350+
placeholder="Your email address"
351+
disabled={false}
352+
value={email}
353+
onChange={(e) => setEmail(e.target.value)}
354+
className="w-full"
355+
containerClassName="!mt-0"
356+
/>
357+
<StyledButton
358+
variant="primary"
359+
disabled={!email}
360+
onClick={() => subscribeToReleaseNotesMutation.mutateAsync(email)}
361+
loading={subscribeToReleaseNotesMutation.isPending}
362+
>
363+
Subscribe
364+
</StyledButton>
365+
</div>
366+
<p className="mt-2 text-sm text-desert-stone-dark">
367+
We care about your privacy. Project N.O.M.A.D. will never share your email with
368+
third parties or send you spam.
369+
</p>
370+
</div>
371+
</div>
372+
</div>
308373
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
309374
<Alert
310375
type="info"

admin/start/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ router
104104
router.post('/services/affect', [SystemController, 'affectService'])
105105
router.post('/services/install', [SystemController, 'installService'])
106106
router.post('/services/force-reinstall', [SystemController, 'forceReinstallService'])
107+
router.post('/subscribe-release-notes', [SystemController, 'subscribeToReleaseNotes'])
107108
router.get('/latest-version', [SystemController, 'checkLatestVersion'])
108109
router.post('/update', [SystemController, 'requestSystemUpdate'])
109110
router.get('/update/status', [SystemController, 'getSystemUpdateStatus'])

0 commit comments

Comments
 (0)