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);
+ };
+}