diff --git a/admin/app/controllers/system_controller.ts b/admin/app/controllers/system_controller.ts index d681a4d..bb76858 100644 --- a/admin/app/controllers/system_controller.ts +++ b/admin/app/controllers/system_controller.ts @@ -1,7 +1,7 @@ import { DockerService } from '#services/docker_service'; import { SystemService } from '#services/system_service' import { SystemUpdateService } from '#services/system_update_service' -import { affectServiceValidator, installServiceValidator } from '#validators/system'; +import { affectServiceValidator, installServiceValidator, subscribeToReleaseNotesValidator } from '#validators/system'; import { inject } from '@adonisjs/core' import type { HttpContext } from '@adonisjs/core/http' @@ -102,4 +102,10 @@ export default class SystemController { const logs = this.systemUpdateService.getUpdateLogs(); response.send({ logs }); } + + + async subscribeToReleaseNotes({ request }: HttpContext) { + const reqData = await request.validateUsing(subscribeToReleaseNotesValidator); + return await this.systemService.subscribeToReleaseNotes(reqData.email); + } } \ No newline at end of file diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index 4a2df42..ad5c713 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -226,6 +226,34 @@ export class SystemService { } } + async subscribeToReleaseNotes(email: string): Promise<{ success: boolean; message: string }> { + try { + const response = await axios.post( + 'https://api.projectnomad.us/api/v1/lists/release-notes/subscribe', + { email }, + { timeout: 5000 } + ) + + if (response.status === 200) { + return { + success: true, + message: 'Successfully subscribed to release notes', + } + } + + return { + success: false, + message: `Failed to subscribe: ${response.statusText}`, + } + } catch (error) { + logger.error('Error subscribing to release notes:', error) + return { + success: false, + message: `Failed to subscribe: ${error instanceof Error ? error.message : error}`, + } + } + } + /** * Checks the current state of Docker containers against the database records and updates the database accordingly. * 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 { const containerExists = serviceStatusList.find( (s) => s.service_name === service.service_name ) - + if (service.installed) { // If marked as installed but container doesn't exist, mark as not installed if (!containerExists) { diff --git a/admin/app/validators/system.ts b/admin/app/validators/system.ts index 8cace3d..fcd92bd 100644 --- a/admin/app/validators/system.ts +++ b/admin/app/validators/system.ts @@ -1,10 +1,20 @@ import vine from '@vinejs/vine' -export const installServiceValidator = vine.compile(vine.object({ - service_name: vine.string().trim() -})); - -export const affectServiceValidator = vine.compile(vine.object({ +export const installServiceValidator = vine.compile( + vine.object({ service_name: vine.string().trim(), - action: vine.enum(['start', 'stop', 'restart']) -})); \ No newline at end of file + }) +) + +export const affectServiceValidator = vine.compile( + vine.object({ + service_name: vine.string().trim(), + action: vine.enum(['start', 'stop', 'restart']), + }) +) + +export const subscribeToReleaseNotesValidator = vine.compile( + vine.object({ + email: vine.string().email().trim(), + }) +) diff --git a/admin/inertia/components/inputs/Input.tsx b/admin/inertia/components/inputs/Input.tsx index d6b1c81..928869b 100644 --- a/admin/inertia/components/inputs/Input.tsx +++ b/admin/inertia/components/inputs/Input.tsx @@ -7,6 +7,7 @@ export interface InputProps extends InputHTMLAttributes { className?: string; labelClassName?: string; inputClassName?: string; + containerClassName?: string; leftIcon?: React.ReactNode; error?: boolean; required?: boolean; @@ -18,6 +19,7 @@ const Input: React.FC = ({ name, labelClassName, inputClassName, + containerClassName, leftIcon, error, required, @@ -31,7 +33,7 @@ const Input: React.FC = ({ > {label}{required ? "*" : ""} -
+
{leftIcon && (
diff --git a/admin/inertia/lib/api.ts b/admin/inertia/lib/api.ts index d90834b..4a66c0d 100644 --- a/admin/inertia/lib/api.ts +++ b/admin/inertia/lib/api.ts @@ -285,6 +285,16 @@ class API { return response.data })() } + + async subscribeToReleaseNotes(email: string) { + return catchInternal(async () => { + const response = await this.client.post<{ success: boolean; message: string }>( + '/system/subscribe-release-notes', + { email } + ) + return response.data + })() + } } export default new API() diff --git a/admin/inertia/pages/settings/update.tsx b/admin/inertia/pages/settings/update.tsx index 09812e2..616571c 100644 --- a/admin/inertia/pages/settings/update.tsx +++ b/admin/inertia/pages/settings/update.tsx @@ -10,6 +10,9 @@ import { useEffect, useState } from 'react' import { IconCircleCheck } from '@tabler/icons-react' import { SystemUpdateStatus } from '../../../types/system' import api from '~/lib/api' +import Input from '~/components/inputs/Input' +import { useMutation } from '@tanstack/react-query' +import { useNotifications } from '~/context/NotificationContext' export default function SystemUpdatePage(props: { system: { @@ -18,11 +21,14 @@ export default function SystemUpdatePage(props: { currentVersion: string } }) { + const { addNotification } = useNotifications() + const [isUpdating, setIsUpdating] = useState(false) const [updateStatus, setUpdateStatus] = useState(null) const [error, setError] = useState(null) const [showLogs, setShowLogs] = useState(false) const [logs, setLogs] = useState('') + const [email, setEmail] = useState('') useEffect(() => { if (!isUpdating) return @@ -116,10 +122,33 @@ export default function SystemUpdatePage(props: { if (updateStatus?.stage === 'error') return if (isUpdating) return - if (props.system.updateAvailable) return + if (props.system.updateAvailable) + return return } + const subscribeToReleaseNotesMutation = useMutation({ + mutationKey: ['subscribeToReleaseNotes'], + mutationFn: (email: string) => api.subscribeToReleaseNotes(email), + onSuccess: (data) => { + if (data && data.success) { + addNotification({ type: 'success', message: 'Successfully subscribed to release notes!' }) + setEmail('') + } else { + addNotification({ + type: 'error', + message: `Failed to subscribe: ${data?.message || 'Unknown error'}`, + }) + } + }, + onError: (error: any) => { + addNotification({ + type: 'error', + message: `Error subscribing to release notes: ${error.message || 'Unknown error'}`, + }) + }, + }) + return ( @@ -128,7 +157,8 @@ export default function SystemUpdatePage(props: {

System Update

- Keep your Project N.O.M.A.D. instance up to date with the latest features and improvements. + Keep your Project N.O.M.A.D. instance up to date with the latest features and + improvements.

@@ -161,9 +191,7 @@ export default function SystemUpdatePage(props: { {!isUpdating && ( <>

- {props.system.updateAvailable - ? 'Update Available' - : 'System Up to Date'} + {props.system.updateAvailable ? 'Update Available' : 'System Up to Date'}

{props.system.updateAvailable @@ -305,6 +333,43 @@ export default function SystemUpdatePage(props: { )}

+
+
+
+

+ Want to stay updated with the latest from Project N.O.M.A.D.? Subscribe to receive + release notes directly to your inbox. Unsubscribe anytime. +

+
+
+
+ setEmail(e.target.value)} + className="w-full" + containerClassName="!mt-0" + /> + subscribeToReleaseNotesMutation.mutateAsync(email)} + loading={subscribeToReleaseNotesMutation.isPending} + > + Subscribe + +
+

+ We care about your privacy. Project N.O.M.A.D. will never share your email with + third parties or send you spam. +

+
+
+