diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6daa8da1..755535d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,7 +128,7 @@ jobs: releaseName: 'v__VERSION__' releaseBody: | ${{ needs.changelog.outputs.CHANGELOG }} - See the assets to download this version and install. + See the assets to download this version and install it. releaseDraft: false prerelease: false diff --git a/apps/client/src/store/NewsStore.spec.ts b/apps/client/src/store/NewsStore.spec.ts index a6edf2ad..f0fe30f8 100644 --- a/apps/client/src/store/NewsStore.spec.ts +++ b/apps/client/src/store/NewsStore.spec.ts @@ -50,7 +50,7 @@ describe('NewsStore', () => { json: async () => ({ changelogs: [ { - id: 1, + id: 'v1', date: '2021-01-01T00:00:00.000Z', text: 'Initial release', }, @@ -65,7 +65,7 @@ describe('NewsStore', () => { expect(fetchSpy).toHaveBeenCalledWith(`${import.meta.env.VITE_SERVER_URL}/changelog`); expect(store.changelogs).toEqual([ { - id: 1, + id: 'v1', date: '2021-01-01T00:00:00.000Z', text: 'Initial release', }, diff --git a/apps/client/src/ui/hud/components/Changelog.tsx b/apps/client/src/ui/hud/components/Changelog.tsx index 8a4484da..2f882305 100644 --- a/apps/client/src/ui/hud/components/Changelog.tsx +++ b/apps/client/src/ui/hud/components/Changelog.tsx @@ -2,6 +2,7 @@ import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import Typography from '@mui/material/Typography'; import { observer } from 'mobx-react-lite'; +import { TimeMgt } from 'shared/src/utils/timeMgt'; import { useStore } from '../../../store'; export const Changelog = observer(() => { @@ -26,8 +27,9 @@ export const Changelog = observer(() => { variant="body1" color="textSecondary" dangerouslySetInnerHTML={{ - __html: `${new Date(date).toLocaleDateString()}
${text}
`, + __html: `${TimeMgt.formatDatetime(new Date(date))}
${text}
`, }} + mb={2} /> ))} diff --git a/apps/server/src/routers/changelogRouter.ts b/apps/server/src/routers/changelogRouter.ts index f0673935..e88cb68b 100644 --- a/apps/server/src/routers/changelogRouter.ts +++ b/apps/server/src/routers/changelogRouter.ts @@ -1,22 +1,32 @@ import { RequestHandler } from 'express'; -import { ChangelogSchema } from 'shared'; -import { prisma } from '../utils/prisma'; +import { ChangelogSchema, fetchGitHubReleases } from 'shared'; + +const CHANGELOG_DEFAULT_MESSAGE = 'See the assets to download this version and install it.'; export const changelogRouter: RequestHandler = async (_, res) => { - const changelogs = await prisma.changelog.findMany({ - orderBy: { - date: 'desc', - }, - take: 10, - }); + try { + const releases = await fetchGitHubReleases(); + + const payload: ChangelogSchema = { + changelogs: releases + .map((release) => ({ + ...release, + body: release.body.replace(new RegExp(CHANGELOG_DEFAULT_MESSAGE, 'g'), ''), + })) + .filter(({ body }) => body !== '') + .map(({ tag_name, published_at, body }) => ({ + id: tag_name, + date: published_at, + text: body, + })), + }; - const result: ChangelogSchema = { - changelogs: changelogs.map((changelog) => ({ - id: changelog.id, - date: changelog.date.toISOString(), - text: changelog.text, - })), - }; + res.send(payload); + } catch (e) { + const payload: ChangelogSchema = { + changelogs: [], + }; - res.send(result); + res.send(payload); + } }; diff --git a/apps/website/pages/api/version.tsx b/apps/website/pages/api/version.tsx index 4095d298..d7047e8f 100644 --- a/apps/website/pages/api/version.tsx +++ b/apps/website/pages/api/version.tsx @@ -1,12 +1,10 @@ import type { NextRequest } from 'next/server'; +import { fetchLatestGitHubRelease } from 'shared'; export const config = { runtime: 'edge', }; -const RELEASE_ENDPOINT = - 'https://api.github.com/repos/matthieu-locussol/taktix-app/releases/latest'; - const ARCHITECTURES = [ 'darwin-aarch64', 'darwin-x86_64', @@ -35,8 +33,7 @@ export interface Version { const handler = async (_: NextRequest) => { try { - const data = await fetch(RELEASE_ENDPOINT); - const json: GitHubRelease = await data.json(); + const { tag_name, published_at, assets } = await fetchLatestGitHubRelease(); const computeSignature = async (url: string) => { const signatureData = await fetch(url); @@ -46,12 +43,12 @@ const handler = async (_: NextRequest) => { return new Response( JSON.stringify({ - version: json.tag_name, - notes: `Taktix ${json.tag_name}`, - pub_date: json.published_at, + version: tag_name, + notes: `Taktix ${tag_name}`, + pub_date: published_at, platforms: ( await Promise.all( - json.assets + assets .filter(({ name }) => ['.AppImage.tar.gz', '.app.tar.gz', '.msi.zip'].some((extension) => name.endsWith(extension), @@ -102,82 +99,3 @@ const handler = async (_: NextRequest) => { }; export default handler; - -export interface GitHubRelease { - url: string; - assets_url: string; - upload_url: string; - html_url: string; - id: number; - author: Author; - node_id: string; - tag_name: string; - target_commitish: string; - name: string; - draft: boolean; - prerelease: boolean; - created_at: string; - published_at: string; - assets: Asset[]; - tarball_url: string; - zipball_url: string; - body: string; -} - -export interface Author { - login: string; - id: number; - node_id: string; - avatar_url: string; - gravatar_id: string; - url: string; - html_url: string; - followers_url: string; - following_url: string; - gists_url: string; - starred_url: string; - subscriptions_url: string; - organizations_url: string; - repos_url: string; - events_url: string; - received_events_url: string; - type: string; - site_admin: boolean; -} - -export interface Asset { - url: string; - id: number; - node_id: string; - name: string; - label: string; - uploader: Uploader; - content_type: string; - state: string; - size: number; - download_count: number; - created_at: string; - updated_at: string; - browser_download_url: string; -} - -export interface Uploader { - login: string; - id: number; - node_id: string; - avatar_url: string; - gravatar_id: string; - url: string; - html_url: string; - followers_url: string; - following_url: string; - gists_url: string; - starred_url: string; - subscriptions_url: string; - organizations_url: string; - repos_url: string; - events_url: string; - received_events_url: string; - type: string; - site_admin: boolean; -} diff --git a/packages/shared/src/data/githubReleases.ts b/packages/shared/src/data/githubReleases.ts new file mode 100644 index 00000000..3ee21745 --- /dev/null +++ b/packages/shared/src/data/githubReleases.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; + +export const GITHUB_RELEASES_ENDPOINT = + 'https://api.github.com/repos/matthieu-locussol/taktix-app/releases'; + +export const GITHUB_LATEST_RELEASE_ENDPOINT = + 'https://api.github.com/repos/matthieu-locussol/taktix-app/releases/latest'; + +export const zGitHubReleaseAsset = z.object({ + name: z.string(), + browser_download_url: z.string(), +}); + +export type GitHubReleaseAsset = z.infer; + +export const zGitHubRelease = z.object({ + tag_name: z.string(), + published_at: z.string(), + assets: z.array(zGitHubReleaseAsset), + body: z.string(), +}); + +export type GitHubRelease = z.infer; + +export const fetchGitHubReleases = async () => { + const response = await fetch(GITHUB_RELEASES_ENDPOINT); + const json = await response.json(); + + return z.array(zGitHubRelease).parse(json); +}; + +export const fetchLatestGitHubRelease = async () => { + const response = await fetch(GITHUB_LATEST_RELEASE_ENDPOINT); + const json = await response.json(); + + return zGitHubRelease.parse(json); +}; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 2aef3e4d..e4316289 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,5 +1,6 @@ export * from './config'; export * from './data/channelsInformations'; +export * from './data/githubReleases'; export * from './data/rolesInformations'; export * from './data/roomsNames'; export * from './data/teleportationSpots'; @@ -27,3 +28,4 @@ export * from './utils/numberMgt'; export * from './utils/permissionMgt'; export * from './utils/stringMgt'; export * from './utils/timeMgt'; +export * from './utils/zodMgt'; diff --git a/packages/shared/src/schemas/ChangelogSchema.ts b/packages/shared/src/schemas/ChangelogSchema.ts index c0c4db01..90dc4545 100644 --- a/packages/shared/src/schemas/ChangelogSchema.ts +++ b/packages/shared/src/schemas/ChangelogSchema.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; export const zChangelogSchema = z.object({ changelogs: z.array( z.object({ - id: z.number(), + id: z.string(), date: z.string(), text: z.string(), }), diff --git a/packages/shared/src/utils/timeMgt.ts b/packages/shared/src/utils/timeMgt.ts index 1c9865ff..7b906e66 100644 --- a/packages/shared/src/utils/timeMgt.ts +++ b/packages/shared/src/utils/timeMgt.ts @@ -3,4 +3,14 @@ export namespace TimeMgt { new Promise((resolve) => { setTimeout(resolve, ms); }); + + export const formatDatetime = (date: Date) => { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + const hours = date.getHours(); + const minutes = date.getMinutes(); + + return `${year}-${month}-${day} @ ${hours}:${minutes}`; + }; } diff --git a/packages/shared/src/utils/zodMgt.spec.ts b/packages/shared/src/utils/zodMgt.spec.ts new file mode 100644 index 00000000..07884991 --- /dev/null +++ b/packages/shared/src/utils/zodMgt.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; +import { z } from 'zod'; +import { ZodMgt } from './zodMgt'; + +describe('ZodMgt', () => { + describe('isValidZodLiteralUnion', () => { + it('should return true if the literals array has at least 2 elements', () => { + const literals = [z.literal('a'), z.literal('b')]; + expect(ZodMgt.isValidZodLiteralUnion(literals)).toBe(true); + }); + + it('should return false if the literals array has less than 2 elements', () => { + const literals = [z.literal('a')]; + expect(ZodMgt.isValidZodLiteralUnion(literals)).toBe(false); + }); + }); + + describe('constructZodLiteralUnionType', () => { + it('should return a ZodUnion schema of the literals passed', () => { + const literals = [z.literal('a'), z.literal('b')]; + const union = ZodMgt.constructZodLiteralUnionType(literals); + + expect(JSON.stringify(union)).toEqual( + JSON.stringify(z.union([z.literal('a'), z.literal('b')])), + ); + }); + + it('should throw an error if the literals array has less than 2 elements', () => { + const literals = [z.literal('a')]; + expect(() => ZodMgt.constructZodLiteralUnionType(literals)).toThrowError( + 'Literals passed do not meet the criteria for constructing a union schema, the minimum length is 2', + ); + }); + }); +}); diff --git a/packages/shared/src/utils/zodMgt.ts b/packages/shared/src/utils/zodMgt.ts new file mode 100644 index 00000000..14c24e19 --- /dev/null +++ b/packages/shared/src/utils/zodMgt.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +export namespace ZodMgt { + export const isValidZodLiteralUnion = >( + literals: T[], + ): literals is [T, T, ...T[]] => literals.length >= 2; + + export const constructZodLiteralUnionType = >(literals: T[]) => { + if (!isValidZodLiteralUnion(literals)) { + throw new Error( + 'Literals passed do not meet the criteria for constructing a union schema, the minimum length is 2', + ); + } + + return z.union(literals); + }; +}