From 66fced40e784113e77ffcde2295aea94f2a6912d Mon Sep 17 00:00:00 2001
From: Jason Rasmussen <jrasm91@gmail.com>
Date: Fri, 31 May 2024 13:13:23 -0400
Subject: [PATCH] docs: roadmap (#9902)

---
 docs/docusaurus.config.js                     |   4 +-
 docs/src/components/timeline.tsx              |  73 +-
 .../src/pages/{milestones.tsx => roadmap.tsx} | 837 +++++++++---------
 docs/static/_redirects                        |   1 +
 4 files changed, 429 insertions(+), 486 deletions(-)
 rename docs/src/pages/{milestones.tsx => roadmap.tsx} (54%)

diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index a9680fb9a1..5962407dcc 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -100,9 +100,9 @@ const config = {
             label: 'Docs',
           },
           {
-            to: '/milestones',
+            to: '/roadmap',
             position: 'right',
-            label: 'Milestones',
+            label: 'Roadmap',
           },
           {
             to: '/docs/api',
diff --git a/docs/src/components/timeline.tsx b/docs/src/components/timeline.tsx
index b4b56bd283..8150d0038a 100644
--- a/docs/src/components/timeline.tsx
+++ b/docs/src/components/timeline.tsx
@@ -1,28 +1,22 @@
-import React from 'react';
-import Icon from '@mdi/react';
-import { mdiCheckboxMarkedCircleOutline } from '@mdi/js';
 import useIsBrowser from '@docusaurus/useIsBrowser';
+import { mdiCheckboxBlankCircle, mdiCheckboxMarkedCircle } from '@mdi/js';
+import Icon from '@mdi/react';
+import React from 'react';
 
-export interface Item {
+export type Item = {
   icon: string;
   title: string;
   description?: string;
-  release?: string;
-  tag?: string;
-  date: Date;
-  dateType: DateType;
-}
-
-export enum DateType {
-  RELEASE = 'Release Date',
-  DATE = 'Date',
-}
+  link?: { url: string; text: string };
+  done?: false;
+  getDateLabel: (language: string) => string;
+};
 
 interface Props {
   items: Item[];
 }
 
-export default function Timeline({ items }: Props): JSX.Element {
+export function Timeline({ items }: Props): JSX.Element {
   const isBrowser = useIsBrowser();
 
   return (
@@ -30,21 +24,15 @@ export default function Timeline({ items }: Props): JSX.Element {
       {items.map((item, index) => {
         const isFirst = index === 0;
         const isLast = index === items.length - 1;
-
-        const classNames: string[] = [];
-
-        if (isFirst) {
-          classNames.push('');
-        }
-
-        if (isLast) {
-          classNames.push('rounded rounded-b-full');
-        }
+        const done = item.done ?? true;
+        const dateLabel = item.getDateLabel(isBrowser ? navigator.language : 'en-US');
+        const timelineIcon = done ? mdiCheckboxMarkedCircle : mdiCheckboxBlankCircle;
+        const cardIcon = item.icon;
 
         return (
-          <li key={index} className="flex min-h-24 w-[700px] max-w-[90vw]">
+          <li key={index} className={`flex min-h-24 w-[700px] max-w-[90vw] ${done ? '' : 'italic'}`}>
             <div className="md:flex justify-start w-36 mr-8 items-center dark:text-immich-dark-primary text-immich-primary hidden">
-              {isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}
+              {dateLabel}
             </div>
             <div className={`${isFirst && 'relative top-[50%]'} ${isLast && 'relative bottom-[50%]'}`}>
               <div
@@ -54,33 +42,26 @@ export default function Timeline({ items }: Props): JSX.Element {
               ></div>
             </div>
             <div className="z-10 flex items-center bg-immich-primary dark:bg-immich-dark-primary border-2 border-solid rounded-full dark:text-black text-white relative top-[50%] left-[-3px] translate-y-[-50%] translate-x-[-50%] w-8 h-8 shadow-lg ">
-              <Icon path={mdiCheckboxMarkedCircleOutline} size={1.25} />
+              {<Icon path={timelineIcon} size={1.25} />}
             </div>
-            <section className=" dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl flex flex-col w-full gap-2 p-4 md:ml-4 my-2 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 transition-all">
-              <div className="m-0 text-lg flex w-full items-center justify-between gap-2">
-                <p className="m-0 items-start flex gap-2">
-                  <Icon path={item.icon} size={1} />
+            <section className=" dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl flex flex-row w-full gap-2 p-4 md:ml-4 my-2 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 transition-all">
+              <div className="flex-col flex-grow items-center justify-between gap-2">
+                <p className="m-0 mb-2 text-lg items-start flex gap-2">
+                  <Icon path={cardIcon} size={1} />
                   <span>{item.title}</span>
                 </p>
-
+                <p className="m-0 text-sm text-gray-600 dark:text-gray-300">{item.description}</p>
+              </div>
+              <div className="flex flex-col justify-between place-items-end">
                 <span className="dark:text-immich-dark-primary text-immich-primary">
-                  {item.tag ? (
-                    <a
-                      href={`https://github.com/immich-app/immich/releases/tag/${item.tag}`}
-                      target="_blank"
-                      rel="noopener"
-                    >
-                      [{item.release ?? item.tag}]{' '}
+                  {item.link && (
+                    <a href={item.link.url} target="_blank" rel="noopener">
+                      [{item.link.text}]
                     </a>
-                  ) : (
-                    item.release && <span>[{item.release}]</span>
                   )}
                 </span>
+                <div className="md:hidden text-sm text-right">{dateLabel}</div>
               </div>
-              <div className="md:hidden text-xs">
-                {`${item.dateType} - ${isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}`}
-              </div>
-              <p className="m-0 text-sm text-gray-600 dark:text-gray-300">{item.description}</p>
             </section>
           </li>
         );
diff --git a/docs/src/pages/milestones.tsx b/docs/src/pages/roadmap.tsx
similarity index 54%
rename from docs/src/pages/milestones.tsx
rename to docs/src/pages/roadmap.tsx
index a69b11670f..cc96495a06 100644
--- a/docs/src/pages/milestones.tsx
+++ b/docs/src/pages/roadmap.tsx
@@ -1,31 +1,43 @@
 import {
-  mdiEyeRefreshOutline,
   mdiAccountGroup,
+  mdiAccountGroupOutline,
   mdiAndroid,
   mdiAppleIos,
   mdiArchiveOutline,
   mdiBash,
   mdiBookSearchOutline,
+  mdiBookmark,
   mdiCakeVariant,
+  mdiCameraBurst,
+  mdiChartBoxMultipleOutline,
   mdiCheckAll,
   mdiCheckboxMarked,
+  mdiCloudUploadOutline,
   mdiCollage,
   mdiContentCopy,
   mdiDevices,
+  mdiEmailOutline,
   mdiExpansionCard,
+  mdiEyeOutline,
+  mdiEyeRefreshOutline,
   mdiFaceMan,
   mdiFaceManOutline,
   mdiFile,
   mdiFileSearch,
+  mdiFlash,
+  mdiFlowerPoppy,
   mdiFolder,
   mdiForum,
+  mdiHandshakeOutline,
   mdiHeart,
   mdiImage,
   mdiImageAlbum,
+  mdiImageEdit,
   mdiImageMultipleOutline,
   mdiImageSearch,
   mdiKeyboardSettingsOutline,
   mdiMagnify,
+  mdiMagnifyScan,
   mdiMap,
   mdiMaterialDesign,
   mdiMatrix,
@@ -36,13 +48,18 @@ import {
   mdiPanVertical,
   mdiPartyPopper,
   mdiPencil,
+  mdiPencilOff,
+  mdiPencilOutline,
   mdiRaw,
+  mdiRocketLaunch,
   mdiRotate360,
+  mdiScaleBalance,
   mdiSecurity,
   mdiServer,
   mdiShareAll,
   mdiShareCircle,
   mdiStar,
+  mdiTableKey,
   mdiTag,
   mdiText,
   mdiThemeLightDark,
@@ -50,663 +67,608 @@ import {
   mdiVectorCombine,
   mdiVideo,
   mdiWeb,
-  mdiScaleBalance,
-  mdiMagnifyScan,
-  mdiChartBoxMultipleOutline,
-  mdiAccountGroupOutline,
-  mdiFlowerPoppy,
-  mdiHandshakeOutline,
 } from '@mdi/js';
 import Layout from '@theme/Layout';
 import React from 'react';
-import Timeline, { DateType, Item } from '../components/timeline';
+import { Item, Timeline } from '../components/timeline';
 
-const items: Item[] = [
+const releases = {
+  'v1.104.0': new Date(2024, 4, 13),
+  'v1.103.0': new Date(2024, 3, 29),
+  'v1.102.0': new Date(2024, 3, 15),
+  'v1.99.0': new Date(2024, 2, 20),
+  'v1.98.0': new Date(2024, 2, 7),
+  'v1.95.0': new Date(2024, 1, 20),
+  'v1.94.0': new Date(2024, 0, 31),
+  'v1.93.0': new Date(2024, 0, 19),
+  'v1.91.0': new Date(2023, 11, 15),
+  'v1.90.0': new Date(2023, 11, 7),
+  'v1.88.0': new Date(2023, 10, 20),
+  'v1.84.0': new Date(2023, 10, 1),
+  'v1.83.0': new Date(2023, 9, 28),
+  'v1.82.0': new Date(2023, 9, 17),
+  'v1.79.0': new Date(2023, 8, 21),
+  'v1.76.0': new Date(2023, 7, 29),
+  'v1.75.0': new Date(2023, 7, 26),
+  'v1.72.0': new Date(2023, 7, 6),
+  'v1.71.0': new Date(2023, 6, 29),
+  'v1.69.0': new Date(2023, 6, 23),
+  'v1.68.0': new Date(2023, 6, 20),
+  'v1.67.0': new Date(2023, 6, 14),
+  'v1.66.0': new Date(2023, 6, 4),
+  'v1.65.0': new Date(2023, 5, 30),
+  'v1.63.0': new Date(2023, 5, 24),
+  'v1.61.0': new Date(2023, 5, 16),
+  'v1.58.0': new Date(2023, 4, 28),
+  'v1.57.0': new Date(2023, 4, 23),
+  'v1.56.0': new Date(2023, 4, 18),
+  'v1.55.0': new Date(2023, 4, 9),
+  'v1.54.0': new Date(2023, 3, 18),
+  'v1.52.0': new Date(2023, 2, 29),
+  'v1.51.0': new Date(2023, 2, 20),
+  'v1.48.0': new Date(2023, 1, 21),
+  'v1.47.0': new Date(2023, 1, 13),
+  'v1.46.0': new Date(2023, 1, 9),
+  'v1.43.0': new Date(2023, 1, 3),
+  'v1.41.0': new Date(2023, 0, 10),
+  'v1.39.0': new Date(2022, 11, 19),
+  'v1.36.0': new Date(2022, 10, 20),
+  'v1.33.1': new Date(2022, 9, 26),
+  'v1.32.0': new Date(2022, 9, 14),
+  'v1.27.0': new Date(2022, 8, 6),
+  'v1.24.0': new Date(2022, 7, 19),
+  'v1.10.0': new Date(2022, 4, 29),
+  'v1.7.0': new Date(2022, 3, 24),
+  'v1.3.0': new Date(2022, 2, 22),
+  'v1.2.0': new Date(2022, 1, 8),
+} as const;
+
+const weirdTags = {
+  'v1.41.0': 'v1.41.1_64-dev',
+  'v1.39.0': 'v1.39.0_61-dev',
+  'v1.36.0': 'v1.36.0_55-dev',
+  'v1.33.1': 'v1.33.0_52-dev',
+  'v1.32.0': 'v1.32.0_50-dev',
+  'v1.27.0': 'v1.27.0_37-dev',
+  'v1.24.0': 'v1.24.0_34-dev',
+  'v1.10.0': 'v1.10.0_15-dev',
+  'v1.7.0': 'v1.7.0_11-dev ',
+  'v1.3.0': 'v1.3.0-dev ',
+  'v1.2.0': 'v0.2-dev ',
+};
+
+const withLanguage = (date: Date) => (language: string) => date.toLocaleDateString(language);
+
+type Base = { icon: string; title: string; description: string };
+const withRelease = ({ icon, title, description, release: version }: Base & { release: keyof typeof releases }) => {
+  return {
+    icon,
+    title,
+    description,
+    link: {
+      url: `https://github.com/immich-app/immich/releases/tag/${weirdTags[version] ?? version}`,
+      text: version,
+    },
+    getDateLabel: withLanguage(releases[version]),
+  };
+};
+
+const roadmap: Item[] = [
+  {
+    done: false,
+    icon: mdiRocketLaunch,
+    title: 'Stable release',
+    description: 'Immich goes stable',
+    getDateLabel: () => 'Planned for 2024',
+  },
+  {
+    done: false,
+    icon: mdiCloudUploadOutline,
+    title: 'Better background backups',
+    description: 'Rework background backups to be more reliable',
+    getDateLabel: () => 'Planned for 2024',
+  },
+  {
+    done: false,
+    icon: mdiImageEdit,
+    title: 'Basic editor',
+    description: 'Basic photo editing capabilities',
+    getDateLabel: () => 'Planned for 2024',
+  },
+  {
+    done: false,
+    icon: mdiFlash,
+    title: 'Workflows',
+    description: 'Automate tasks with workflows',
+    getDateLabel: () => 'Planned for 2024',
+  },
+  {
+    done: false,
+    icon: mdiTableKey,
+    title: 'Fine grained access controls',
+    description: 'Granular access controls for users and api keys',
+    getDateLabel: () => 'Planned for 2024',
+  },
+  {
+    done: false,
+    icon: mdiWeb,
+    title: 'Web translations',
+    description: 'Translate the web application to multiple languages',
+    getDateLabel: () => 'Planned for 2024',
+  },
+  {
+    done: false,
+    icon: mdiCameraBurst,
+    title: 'Auto stacking',
+    description: 'Auto stack burst photos',
+    getDateLabel: () => 'Planned for 2024',
+  },
+];
+
+const milestones: Item[] = [
+  // withRelease({
+  //   icon: mdiVectorCombine,
+  //   title: 'Container consolidation',
+  //   description:
+  //     'The microservices container can be run as a worker within the server image, allowing us to remove it from the default stack.',
+  //   release: 'v1.106.0',
+  // }),
+  withRelease({
+    icon: mdiPencilOutline,
+    title: 'Read-write external libraries',
+    description: 'Edit, update, and delete files in external libraries',
+    release: 'v1.104.0',
+  }),
+  withRelease({
+    icon: mdiEmailOutline,
+    title: 'Email notifications',
+    description: 'Send emails for important events',
+    release: 'v1.104.0',
+  }),
   {
     icon: mdiHandshakeOutline,
-    description: 'Joined Futo and Immich core team goes full-time',
     title: 'Immich joins FUTO!',
-    date: new Date(2024, 4, 1),
-    dateType: DateType.DATE,
+    description: 'Joined Futo and Immich core team goes full-time',
+    getDateLabel: withLanguage(new Date(2024, 4, 1)),
   },
-  {
+  withRelease({
+    icon: mdiEyeOutline,
+    title: 'Read-only albums',
+    description: 'Share albums with other users as read-only',
+    release: 'v1.103.0',
+  }),
+  withRelease({
+    icon: mdiBookmark,
+    title: 'Permanent URLs (Web)',
+    description: 'Assets on the web now have permanent URLs',
+    release: 'v1.103.0',
+  }),
+  withRelease({
     icon: mdiStar,
-    description: 'Reached 30K Stars on GitHub!',
     title: '30,000 Stars',
+    description: 'Reached 30K Stars on GitHub!',
     release: 'v1.102.0',
-    tag: 'v1.102.0',
-    date: new Date(2024, 3, 15),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiChartBoxMultipleOutline,
-    description: 'OpenTelemetry metrics for local evaluation and advanced debugging',
     title: 'OpenTelemetry metrics',
+    description: 'OpenTelemetry metrics for local evaluation and advanced debugging',
     release: 'v1.99.0',
-    tag: 'v1.99.0',
-    date: new Date(2024, 2, 20),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiFlowerPoppy,
-    description: 'Immich got its new logo',
     title: 'New logo',
+    description: 'Immich got its new logo',
     release: 'v1.98.0',
-    tag: 'v1.98.0',
-    date: new Date(2024, 2, 7),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMagnifyScan,
-    description: 'Advanced search with filters by date, location and more',
     title: 'Search enhancement with advanced filters',
+    description: 'Advanced search with filters by date, location and more',
     release: 'v1.95.0',
-    tag: 'v1.95.0',
-    date: new Date(2024, 1, 20),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiScaleBalance,
-    description: 'Immich switches to AGPLv3 license',
     title: 'AGPL License',
+    description: 'Immich switches to AGPLv3 license',
     release: 'v1.95.0',
-    tag: 'v1.95.0',
-    date: new Date(2024, 1, 20),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiEyeRefreshOutline,
-    description: 'Automatically import files in external libraries when the operating system detects changes.',
     title: 'Library watching',
+    description: 'Automatically import files in external libraries when the operating system detects changes.',
     release: 'v1.94.0',
-    tag: 'v1.94.0',
-    date: new Date(2024, 0, 31),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiExpansionCard,
-    description: 'Hardware acceleration support for Nvidia and Intel devices through CUDA and OpenVINO.',
     title: 'GPU acceleration for machine-learning',
+    description: 'Hardware acceleration support for Nvidia and Intel devices through CUDA and OpenVINO.',
     release: 'v1.94.0',
-    tag: 'v1.94.0',
-    date: new Date(2024, 0, 31),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiAccountGroupOutline,
-    description: '250 amazing people contributed to Immich',
     title: '250 unique contributors',
+    description: '250 amazing people contributed to Immich',
     release: 'v1.93.0',
-    tag: 'v1.93.0',
-    date: new Date(2024, 0, 19),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMatrix,
-    description: 'Moved the search from typesense to pgvecto.rs',
     title: 'Search improvement with pgvecto.rs',
+    description: 'Moved the search from typesense to pgvecto.rs',
     release: 'v1.91.0',
-    tag: 'v1.91.0',
-    date: new Date(2023, 11, 15),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiPencil,
-    description: "Edit a photo or video's date, time, hours, timezone, and GPS information",
     title: 'Edit metadata',
+    description: "Edit a photo or video's date, time, hours, timezone, and GPS information",
     release: 'v1.90.0',
-    tag: 'v1.90.0',
-    date: new Date(2023, 11, 7),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiVectorCombine,
+    title: 'Container consolidation',
     description:
       'The serving of the web app is merged into the server image, allowing us to remove two containers from the stack.',
-    title: 'Container consolidation',
     release: 'v1.88.0',
-    tag: 'v1.88.0',
-    date: new Date(2023, 10, 20),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiBash,
-    description: 'Version 2 of the Immich CLI is released, replacing the legacy v1 CLI.',
     title: 'CLI v2',
+    description: 'Version 2 of the Immich CLI is released, replacing the legacy v1 CLI.',
     release: 'v1.88.0',
-    tag: 'v1.88.0',
-    date: new Date(2023, 10, 19),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiForum,
-    description: 'Comment a photo or a video in a shared album',
     title: 'Activity',
+    description: 'Comment a photo or a video in a shared album',
     release: 'v1.84.0',
-    tag: 'v1.84.0',
-    date: new Date(2023, 10, 1),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiStar,
-    description: 'Reached 20K Stars on GitHub!',
     title: '20,000 Stars',
+    description: 'Reached 20K Stars on GitHub!',
     release: 'v1.83.0',
-    tag: 'v1.83.0',
-    date: new Date(2023, 9, 28),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiContentCopy,
     title: 'Stack assets',
     description: 'Manual asset stacking for grouping and hiding related assets in the main timeline.',
     release: 'v1.83.0',
-    tag: 'v1.83.0',
-    date: new Date(2023, 9, 28),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiPalette,
     title: 'Custom theme',
     description: 'Apply your custom CSS for modifying fonts, colors, and styles in the web application.',
     release: 'v1.83.0',
-    tag: 'v1.83.0',
-    date: new Date(2023, 9, 28),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiTrashCanOutline,
-    title: 'Trash Feature',
+    title: 'Trash feature',
     description: 'Trash, restore from trash, and automatically empty the recycle bin after 30 days.',
     release: 'v1.82.0',
-    tag: 'v1.82.0',
-    date: new Date(2023, 9, 17),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiBookSearchOutline,
-    title: 'External Libraries',
+    title: 'External libraries',
     description: 'Automatically import media into Immich based on imports paths and ignore patterns.',
     release: 'v1.79.0',
-    tag: 'v1.79.0',
-    date: new Date(2023, 8, 21),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMap,
-    title: 'Map View (Mobile)',
+    title: 'Map view (mobile)',
     description: 'Heat map implementation in the mobile app.',
     release: 'v1.76.0',
-    tag: 'v1.76.0',
-    date: new Date(2023, 7, 29),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiFile,
-    title: 'Configuration File',
+    title: 'Configuration file',
     description: 'Auto-configure an Immich installation via a configuration file.',
     release: 'v1.75.0',
-    tag: 'v1.75.0',
-    date: new Date(2023, 7, 26),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMonitor,
-    title: 'Slideshow Mode (Web)',
+    title: 'Slideshow mode (web)',
     description: 'Start a full-screen slideshow from an Album on the web.',
     release: 'v1.75.0',
-    tag: 'v1.75.0',
-    date: new Date(2023, 7, 26),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiServer,
-    title: 'Hardware Transcoding',
+    title: 'Hardware transcoding',
     description: 'Support hardware acceleration (QuickSync, VAAPI, and Nvidia) for video transcoding.',
     release: 'v1.72.0',
-    tag: 'v1.72.0',
-    date: new Date(2023, 7, 6),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiImageAlbum,
-    title: 'View Albums via Time Buckets',
+    title: 'View albums via time buckets',
     description: 'Upgrade albums to use time buckets, an optimized virtual viewport.',
     release: 'v1.72.0',
-    tag: 'v1.72.0',
-    date: new Date(2023, 7, 6),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiImageAlbum,
-    title: 'Album Description',
+    title: 'Album description',
     description: 'Save an album description.',
     release: 'v1.72.0',
-    tag: 'v1.72.0',
-    date: new Date(2023, 7, 6),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiRotate360,
-    title: '360° Photos (Web)',
+    title: '360° Photos (web)',
     description: 'View 360° Photos on the web.',
     release: 'v1.71.0',
-    tag: 'v1.71.0',
-    date: new Date(2023, 6, 29),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMotionPlayOutline,
-    title: 'Android Motion Photos',
+    title: 'Android motion photos',
     description: 'Add support for Android Motion Photos.',
     release: 'v1.69.0',
-    tag: 'v1.69.0',
-    date: new Date(2023, 6, 23),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiFaceManOutline,
-    title: 'Show/Hide Faces',
+    title: 'Show/hide faces',
     description: 'Add the options to show or hide faces.',
     release: 'v1.68.0',
-    tag: 'v1.68.0',
-    date: new Date(2023, 6, 20),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMerge,
-    title: 'Merge Faces',
+    title: 'Merge faces',
     description: 'Add the ability to merge multiple faces together.',
     release: 'v1.67.0',
-    tag: 'v1.67.0',
-    date: new Date(2023, 6, 14),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiImage,
-    title: 'Feature Photo',
+    title: 'Feature photo',
     description: 'Add the option to change the feature photo for a person.',
     release: 'v1.66.0',
-    tag: 'v1.66.0',
-    date: new Date(2023, 6, 4),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiKeyboardSettingsOutline,
-    title: 'Multi-Select via SHIFT',
+    title: 'Multi-select via SHIFT',
     description: 'Add the option to multi-select while holding SHIFT.',
     release: 'v1.66.0',
-    tag: 'v1.66.0',
-    date: new Date(2023, 6, 4),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiImageMultipleOutline,
-    title: 'Memories (Mobile)',
+    title: 'Memories (mobile)',
     description: 'View "On this day..." memories in the mobile app.',
     release: 'v1.65.0',
-    tag: 'v1.65.0',
-    date: new Date(2023, 5, 30),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiFaceMan,
-    title: 'Facial Recognition (Mobile)',
+    title: 'Facial recognition (mobile)',
     description: 'View detected faces in the mobile app.',
     release: 'v1.63.0',
-    tag: 'v1.63.0',
-    date: new Date(2023, 5, 24),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiImageMultipleOutline,
-    title: 'Memories (Web)',
+    title: 'Memories (web)',
     description: 'View pictures taken in past years on this day on the web.',
     release: 'v1.61.0',
-    tag: 'v1.61.0',
-    date: new Date(2023, 5, 16),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiCollage,
-    title: 'Justified Layout (Web)',
+    title: 'Justified layout (web)',
     description: 'Implement justified layout (collage) on the web.',
     release: 'v1.61.0',
-    tag: 'v1.61.0',
-    date: new Date(2023, 5, 16),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiRaw,
-    title: 'RAW File Formats',
+    title: 'RAW file formats',
     description: 'Support for RAW file formats.',
     release: 'v1.61.0',
-    tag: 'v1.61.0',
-    date: new Date(2023, 5, 16),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiShareAll,
-    title: 'Partner Sharing (Mobile)',
+    title: 'Partner sharing (mobile)',
     description: 'View shared partner photos in the mobile app.',
     release: 'v1.58.0',
-    tag: 'v1.58.0',
-    date: new Date(2023, 4, 28),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiFile,
-    title: 'XMP Sidecar',
-    description: 'Attach XMP Sidecar files to assets.',
+    title: 'XMP sidecar',
+    description: 'Attach XMP sidecar files to assets.',
     release: 'v1.58.0',
-    tag: 'v1.58.0',
-    date: new Date(2023, 4, 28),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiFolder,
-    title: 'Custom Storage Label',
+    title: 'Custom storage label',
     description: 'Replace the user UUID in the storage template with a custom label.',
     release: 'v1.57.0',
-    tag: 'v1.57.0',
-    date: new Date(2023, 4, 23),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiShareCircle,
-    title: 'Partner Sharing',
+    title: 'Partner sharing',
     description: 'Share your entire collection with another user.',
     release: 'v1.56.0',
-    tag: 'v1.56.0',
-    date: new Date(2023, 4, 18),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiFaceMan,
-    title: 'Facial Recognition',
+    title: 'Facial recognition',
     description: 'Detect faces in pictures and cluster them together as people, which can be named.',
     release: 'v1.56.0',
-    tag: 'v1.56.0',
-    date: new Date(2023, 4, 18),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMap,
-    title: 'Map View (Web)',
+    title: 'Map view (web)',
     description: 'View a global map, with clusters of photos based on corresponding GPS data.',
     release: 'v1.55.0',
-    tag: 'v1.55.0',
-    date: new Date(2023, 4, 9),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiDevices,
-    title: 'Manage Auth Devices',
+    title: 'Manage auth devices',
     description: 'Manage logged-in devices and revoke access from User Settings.',
     release: 'v1.55.0',
-    tag: 'v1.55.0',
-    date: new Date(2023, 4, 9),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiStar,
-    description: 'Reached 10K Stars on GitHub!',
     title: '10,000 Stars',
+    description: 'Reached 10K stars on GitHub!',
     release: 'v1.54.0',
-    tag: 'v1.54.0',
-    date: new Date(2023, 3, 18),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiText,
-    title: 'Asset Descriptions',
+    title: 'Asset descriptions',
     description: 'Save an asset description',
     release: 'v1.54.0',
-    tag: 'v1.54.0',
-    date: new Date(2023, 3, 18),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiArchiveOutline,
     title: 'Archiving',
     description: 'Remove assets from the main timeline by archiving them.',
     release: 'v1.54.0',
-    tag: 'v1.54.0',
-    date: new Date(2023, 3, 18),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiDevices,
-    title: 'Responsive Web App',
+    title: 'Responsive web app',
     description: 'Optimize the web app for small screen.',
     release: 'v1.54.0',
-    tag: 'v1.54.0',
-    date: new Date(2023, 3, 18),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiFileSearch,
-    title: 'Search By Metadata',
+    title: 'Search by metadata',
     description: 'Search images by filename, description, tagged people, make, model, and other metadata.',
     release: 'v1.52.0',
-    tag: 'v1.52.0',
-    date: new Date(2023, 2, 29),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiImageSearch,
-    title: 'CLIP Search',
+    title: 'CLIP search',
     description: 'Search images with free-form text like "Sunset at the beach".',
     release: 'v1.51.0',
-    tag: 'v1.51.0',
-    date: new Date(2023, 2, 20),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMagnify,
-    title: 'Explore Page',
+    title: 'Explore page',
     description: 'View tagged places, object, and people.',
     release: 'v1.51.0',
-    tag: 'v1.51.0',
-    date: new Date(2023, 2, 20),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiAppleIos,
-    title: 'iOS Background Uploads',
+    title: 'iOS background uploads',
     description: 'Automatically backup pictures in the background on iOS.',
     release: 'v1.48.0',
-    tag: 'v1.48.0',
-    date: new Date(2023, 1, 21),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMotionPlayOutline,
-    title: 'Auto-Link Live Photos',
+    title: 'Auto-Link live photos',
     description: 'Automatically link live photos, even when uploaded as separate files.',
     release: 'v1.48.0',
-    tag: 'v1.48.0',
-    date: new Date(2023, 2, 21),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMaterialDesign,
-    title: 'Material Design 3 (Mobile)',
+    title: 'Material design 3 (mobile)',
     description: 'Upgrade the mobile app to Material Design 3.',
     release: 'v1.47.0',
-    tag: 'v1.47.0',
-    date: new Date(2023, 1, 13),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiHeart,
-    title: 'Favorites (Mobile)',
+    title: 'Favorites (mobile)',
     description: 'Show favorites on the mobile app.',
     release: 'v1.46.0',
-    tag: 'v1.46.0',
-    date: new Date(2023, 1, 9),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiCakeVariant,
-    title: 'Immich Turns 1',
+    title: 'Immich turns 1',
     description: 'Immich is officially one year old.',
     release: 'v1.43.0',
-    tag: 'v1.43.0',
-    date: new Date(2023, 1, 3),
-    dateType: DateType.DATE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiHeart,
-    title: 'Favorites Page (Web)',
+    title: 'Favorites page (web)',
     description: 'Favorite and view favorites on the web.',
     release: 'v1.43.0',
-    tag: 'v1.43.0',
-    date: new Date(2023, 0, 27),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiShareCircle,
-    title: 'Public Share Links',
+    title: 'Public share links',
     description: 'Share photos and albums publicly via a shared link.',
     release: 'v1.41.0',
-    tag: 'v1.41.1_64-dev',
-    date: new Date(2023, 0, 10),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiFolder,
-    title: 'User-Defined Storage Structure',
+    title: 'User-defined storage structure',
     description: 'Support custom storage structures.',
     release: 'v1.39.0',
-    tag: 'v1.39.0_61-dev',
-    date: new Date(2022, 11, 19),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiMotionPlayOutline,
-    title: 'iOS Live Photos',
+    title: 'iOS live photos',
     description: 'Backup and display iOS Live Photos.',
     release: 'v1.36.0',
-    tag: 'v1.36.0_55-dev',
-    date: new Date(2022, 10, 20),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiSecurity,
-    title: 'OAuth Integration',
+    title: 'OAuth integration',
     description: 'Support OAuth2 and OIDC capable identity providers.',
     release: 'v1.36.0',
-    tag: 'v1.36.0_55-dev',
-    date: new Date(2022, 10, 20),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiWeb,
-    title: 'Documentation Site',
+    title: 'Documentation site',
     description: 'Release an official documentation website.',
     release: 'v1.33.1',
-    tag: 'v1.33.0_52-dev',
-    date: new Date(2022, 9, 26),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiThemeLightDark,
-    title: 'Dark Mode (Web)',
+    title: 'Dark mode (web)',
     description: 'Dark mode on the web.',
     release: 'v1.32.0',
-    tag: 'v1.32.0_50-dev',
-    date: new Date(2022, 9, 14),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiPanVertical,
-    title: 'Virtual Scrollbar (Web)',
+    title: 'Virtual scrollbar (web)',
     description: 'View the main timeline with a virtual scrollbar, allowing to jump to any point in time, instantly.',
     release: 'v1.27.0',
-    tag: 'v1.27.0_37-dev',
-    date: new Date(2022, 8, 6),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiCheckAll,
-    title: 'Checksum Duplication Check',
+    title: 'Checksum duplication check',
     description: 'Enforce per user sha1 checksum uniqueness.',
     release: 'v1.27.0',
-    tag: 'v1.27.0_37-dev',
-    date: new Date(2022, 8, 6),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiAndroid,
-    title: 'Android Background Backup',
+    title: 'Android background backup',
     description: 'Automatic backup in the background on Android.',
     release: 'v1.24.0',
-    tag: 'v1.24.0_34-dev',
-    date: new Date(2022, 7, 19),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiAccountGroup,
-    title: 'Admin Portal',
+    title: 'Admin portal',
     description: 'Manage users and admin settings from the web.',
     release: 'v1.10.0',
-    tag: 'v1.10.0_15-dev',
-    date: new Date(2022, 4, 29),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiShareCircle,
-    title: 'Album Sharing',
+    title: 'Album sharing',
     description: 'Share albums with other users.',
     release: 'v1.7.0',
-    tag: 'v1.7.0_11-dev ',
-    date: new Date(2022, 3, 24),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiTag,
-    title: 'Image Tagging',
+    title: 'Image tagging',
     description: 'Tag images with custom values.',
     release: 'v1.7.0',
-    tag: 'v1.7.0_11-dev ',
-    date: new Date(2022, 3, 24),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiImage,
-    title: 'View Exif',
+    title: 'View exif',
     description: 'View metadata about assets.',
     release: 'v1.3.0',
-    tag: 'v1.3.0-dev ',
-    date: new Date(2022, 2, 22),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiCheckboxMarked,
-    title: 'Multi Select',
+    title: 'Multi select',
     description: 'Select and execute actions on multiple assets at the same time.',
     release: 'v1.2.0',
-    tag: 'v0.2-dev ',
-    date: new Date(2022, 1, 8),
-    dateType: DateType.RELEASE,
-  },
-  {
+  }),
+  withRelease({
     icon: mdiVideo,
-    title: 'Video Player',
+    title: 'Video player',
     description: 'Play videos in the web and on mobile.',
     release: 'v1.2.0',
-    tag: 'v0.2-dev ',
-    date: new Date(2022, 1, 8),
-    dateType: DateType.RELEASE,
-  },
+  }),
   {
     icon: mdiPartyPopper,
-    title: 'First Commit',
+    title: 'First commit',
     description: 'First commit on GitHub, Immich is born.',
-    release: 'v1.0.0',
-    date: new Date(2022, 1, 3),
-    dateType: DateType.DATE,
+    getDateLabel: withLanguage(new Date(2022, 1, 3)),
   },
 ];
 
@@ -715,14 +677,13 @@ export default function MilestonePage(): JSX.Element {
     <Layout title="Milestones" description="History of Immich">
       <section className="my-8">
         <h1 className="md:text-6xl text-center mb-10 text-immich-primary dark:text-immich-dark-primary px-2">
-          Major Milestones
+          Roadmap
         </h1>
         <p className="text-center text-xl px-2">
-          A list of project achievements and milestones, <br />
-          by release date.
+          A list of future plans and goals, as well as past achievements and milestones.
         </p>
         <div className="flex justify-around mt-8 w-full max-w-full">
-          <Timeline items={items} />
+          <Timeline items={[...roadmap, ...milestones]} />
         </div>
       </section>
     </Layout>
diff --git a/docs/static/_redirects b/docs/static/_redirects
index 8b02fe11b9..df45fc95e8 100644
--- a/docs/static/_redirects
+++ b/docs/static/_redirects
@@ -28,3 +28,4 @@
 /docs/features/search /docs/features/smart-search 301
 /docs/guides/api-album-sync /docs/community-projects 301
 /docs/guides/remove-offline-files /docs/community-projects 301
+/milestones /roadmap 301