diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl
index 97234176b..0a8f427f9 100644
--- a/templates/user/dashboard/repolist.tmpl
+++ b/templates/user/dashboard/repolist.tmpl
@@ -1,181 +1,54 @@
-
-	
-
+
+
+
diff --git a/web_src/js/components/DashboardRepoList.js b/web_src/js/components/DashboardRepoList.js
deleted file mode 100644
index 2328cc83a..000000000
--- a/web_src/js/components/DashboardRepoList.js
+++ /dev/null
@@ -1,345 +0,0 @@
-import {createApp, nextTick} from 'vue';
-import $ from 'jquery';
-import {initVueSvg, vueDelimiters} from './VueComponentLoader.js';
-import {initTooltip} from '../modules/tippy.js';
-
-const {appSubUrl, assetUrlPrefix, pageData} = window.config;
-
-function initVueComponents(app) {
-  app.component('repo-search', {
-    delimiters: vueDelimiters,
-    props: {
-      searchLimit: {
-        type: Number,
-        default: 10
-      },
-      subUrl: {
-        type: String,
-        required: true
-      },
-      uid: {
-        type: Number,
-        default: 0
-      },
-      teamId: {
-        type: Number,
-        required: false,
-        default: 0
-      },
-      organizations: {
-        type: Array,
-        default: () => [],
-      },
-      isOrganization: {
-        type: Boolean,
-        default: true
-      },
-      canCreateOrganization: {
-        type: Boolean,
-        default: false
-      },
-      organizationsTotalCount: {
-        type: Number,
-        default: 0
-      },
-      moreReposLink: {
-        type: String,
-        default: ''
-      }
-    },
-
-    data() {
-      const params = new URLSearchParams(window.location.search);
-
-      let tab = params.get('repo-search-tab');
-      if (!tab) {
-        tab = 'repos';
-      }
-
-      let reposFilter = params.get('repo-search-filter');
-      if (!reposFilter) {
-        reposFilter = 'all';
-      }
-
-      let privateFilter = params.get('repo-search-private');
-      if (!privateFilter) {
-        privateFilter = 'both';
-      }
-
-      let archivedFilter = params.get('repo-search-archived');
-      if (!archivedFilter) {
-        archivedFilter = 'unarchived';
-      }
-
-      let searchQuery = params.get('repo-search-query');
-      if (!searchQuery) {
-        searchQuery = '';
-      }
-
-      let page = 1;
-      try {
-        page = parseInt(params.get('repo-search-page'));
-      } catch {
-        // noop
-      }
-      if (!page) {
-        page = 1;
-      }
-
-      return {
-        hasMounted: false, // accessing $refs in computed() need to wait for mounted
-        tab,
-        repos: [],
-        reposTotalCount: 0,
-        reposFilter,
-        archivedFilter,
-        privateFilter,
-        page,
-        finalPage: 1,
-        searchQuery,
-        isLoading: false,
-        staticPrefix: assetUrlPrefix,
-        counts: {},
-        repoTypes: {
-          all: {
-            searchMode: '',
-          },
-          forks: {
-            searchMode: 'fork',
-          },
-          mirrors: {
-            searchMode: 'mirror',
-          },
-          sources: {
-            searchMode: 'source',
-          },
-          collaborative: {
-            searchMode: 'collaborative',
-          },
-        }
-      };
-    },
-
-    computed: {
-      // used in `repolist.tmpl`
-      showMoreReposLink() {
-        return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
-      },
-      searchURL() {
-        return `${this.subUrl}/repo/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery
-        }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode
-        }${this.reposFilter !== 'all' ? '&exclusive=1' : ''
-        }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : ''
-        }${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : ''
-        }`;
-      },
-      repoTypeCount() {
-        return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
-      },
-      checkboxArchivedFilterTitle() {
-        return this.hasMounted && this.$refs.checkboxArchivedFilter?.getAttribute(`data-title-${this.archivedFilter}`);
-      },
-      checkboxArchivedFilterProps() {
-        return {checked: this.archivedFilter === 'archived', indeterminate: this.archivedFilter === 'both'};
-      },
-      checkboxPrivateFilterTitle() {
-        return this.hasMounted && this.$refs.checkboxPrivateFilter?.getAttribute(`data-title-${this.privateFilter}`);
-      },
-      checkboxPrivateFilterProps() {
-        return {checked: this.privateFilter === 'private', indeterminate: this.privateFilter === 'both'};
-      },
-    },
-
-    mounted() {
-      const el = document.getElementById('dashboard-repo-list');
-      this.changeReposFilter(this.reposFilter);
-      for (const elTooltip of el.querySelectorAll('.tooltip')) {
-        initTooltip(elTooltip);
-      }
-      $(el).find('.dropdown').dropdown();
-      nextTick(() => {
-        this.$refs.search.focus();
-      });
-
-      this.hasMounted = true;
-    },
-
-    methods: {
-      changeTab(t) {
-        this.tab = t;
-        this.updateHistory();
-      },
-
-      changeReposFilter(filter) {
-        this.reposFilter = filter;
-        this.repos = [];
-        this.page = 1;
-        this.counts[`${filter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
-        this.searchRepos();
-      },
-
-      updateHistory() {
-        const params = new URLSearchParams(window.location.search);
-
-        if (this.tab === 'repos') {
-          params.delete('repo-search-tab');
-        } else {
-          params.set('repo-search-tab', this.tab);
-        }
-
-        if (this.reposFilter === 'all') {
-          params.delete('repo-search-filter');
-        } else {
-          params.set('repo-search-filter', this.reposFilter);
-        }
-
-        if (this.privateFilter === 'both') {
-          params.delete('repo-search-private');
-        } else {
-          params.set('repo-search-private', this.privateFilter);
-        }
-
-        if (this.archivedFilter === 'unarchived') {
-          params.delete('repo-search-archived');
-        } else {
-          params.set('repo-search-archived', this.archivedFilter);
-        }
-
-        if (this.searchQuery === '') {
-          params.delete('repo-search-query');
-        } else {
-          params.set('repo-search-query', this.searchQuery);
-        }
-
-        if (this.page === 1) {
-          params.delete('repo-search-page');
-        } else {
-          params.set('repo-search-page', `${this.page}`);
-        }
-
-        const queryString = params.toString();
-        if (queryString) {
-          window.history.replaceState({}, '', `?${queryString}`);
-        } else {
-          window.history.replaceState({}, '', window.location.pathname);
-        }
-      },
-
-      toggleArchivedFilter() {
-        if (this.archivedFilter === 'unarchived') {
-          this.archivedFilter = 'archived';
-        } else if (this.archivedFilter === 'archived') {
-          this.archivedFilter = 'both';
-        } else { // including both
-          this.archivedFilter = 'unarchived';
-        }
-        this.page = 1;
-        this.repos = [];
-        this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
-        this.searchRepos();
-      },
-
-      togglePrivateFilter() {
-        if (this.privateFilter === 'both') {
-          this.privateFilter = 'public';
-        } else if (this.privateFilter === 'public') {
-          this.privateFilter = 'private';
-        } else { // including private
-          this.privateFilter = 'both';
-        }
-        this.page = 1;
-        this.repos = [];
-        this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
-        this.searchRepos();
-      },
-
-
-      changePage(page) {
-        this.page = page;
-        if (this.page > this.finalPage) {
-          this.page = this.finalPage;
-        }
-        if (this.page < 1) {
-          this.page = 1;
-        }
-        this.repos = [];
-        this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
-        this.searchRepos();
-      },
-
-      async searchRepos() {
-        this.isLoading = true;
-
-        const searchedMode = this.repoTypes[this.reposFilter].searchMode;
-        const searchedURL = this.searchURL;
-        const searchedQuery = this.searchQuery;
-
-        let response, json;
-        try {
-          if (!this.reposTotalCount) {
-            const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
-            response = await fetch(totalCountSearchURL);
-            this.reposTotalCount = response.headers.get('X-Total-Count');
-          }
-
-          response = await fetch(searchedURL);
-          json = await response.json();
-        } catch {
-          if (searchedURL === this.searchURL) {
-            this.isLoading = false;
-          }
-          return;
-        }
-
-        if (searchedURL === this.searchURL) {
-          this.repos = json.data;
-          const count = response.headers.get('X-Total-Count');
-          if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
-            this.reposTotalCount = count;
-          }
-          this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = count;
-          this.finalPage = Math.ceil(count / this.searchLimit);
-          this.updateHistory();
-          this.isLoading = false;
-        }
-      },
-
-      repoIcon(repo) {
-        if (repo.fork) {
-          return 'octicon-repo-forked';
-        } else if (repo.mirror) {
-          return 'octicon-mirror';
-        } else if (repo.template) {
-          return `octicon-repo-template`;
-        } else if (repo.private) {
-          return 'octicon-lock';
-        } else if (repo.internal) {
-          return 'octicon-repo';
-        }
-        return 'octicon-repo';
-      }
-    },
-
-    template: document.getElementById('dashboard-repo-list-template'),
-  });
-}
-
-export function initDashboardRepoList() {
-  const el = document.getElementById('dashboard-repo-list');
-  const dashboardRepoListData = pageData.dashboardRepoList || null;
-  if (!el || !dashboardRepoListData) return;
-
-  const app = createApp({
-    delimiters: vueDelimiters,
-    data() {
-      return {
-        searchLimit: dashboardRepoListData.searchLimit || 0,
-        subUrl: appSubUrl,
-        uid: dashboardRepoListData.uid || 0,
-      };
-    },
-  });
-  initVueSvg(app);
-  initVueComponents(app);
-  app.mount(el);
-}
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
new file mode 100644
index 000000000..e295910fd
--- /dev/null
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -0,0 +1,432 @@
+
+  
+
+
+
diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue
index 37b6df918..294ee6f7b 100644
--- a/web_src/js/components/RepoActivityTopAuthors.vue
+++ b/web_src/js/components/RepoActivityTopAuthors.vue
@@ -51,7 +51,7 @@
 
 
diff --git a/web_src/js/components/RepoBranchTagDropdown.js b/web_src/js/components/RepoBranchTagDropdown.js
index e1bf35c12..a8945b82d 100644
--- a/web_src/js/components/RepoBranchTagDropdown.js
+++ b/web_src/js/components/RepoBranchTagDropdown.js
@@ -1,6 +1,5 @@
 import {createApp, nextTick} from 'vue';
 import $ from 'jquery';
-import {vueDelimiters} from './VueComponentLoader.js';
 
 export function initRepoBranchTagDropdown(selector) {
   $(selector).each(function (dropdownIndex, elRoot) {
@@ -39,7 +38,7 @@ export function initRepoBranchTagDropdown(selector) {
     }
 
     const view = createApp({
-      delimiters: vueDelimiters,
+      delimiters: ['${', '}'],
       data() {
         return data;
       },
diff --git a/web_src/js/components/VueComponentLoader.js b/web_src/js/components/VueComponentLoader.js
deleted file mode 100644
index 33ebf95ef..000000000
--- a/web_src/js/components/VueComponentLoader.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import {createApp} from 'vue';
-import {svgs} from '../svg.js';
-
-export const vueDelimiters = ['${', '}'];
-
-let vueEnvInited = false;
-export function initVueEnv() {
-  if (vueEnvInited) return;
-  vueEnvInited = true;
-
-  // As far as I could tell, this is no longer possible.
-  // But there seem not to be a guide what to do instead.
-  // const isProd = window.config.runModeIsProd;
-  // Vue.config.devtools = !isProd;
-}
-
-let vueSvgInited = false;
-export function initVueSvg(app) {
-  if (vueSvgInited) return;
-  vueSvgInited = true;
-
-  // register svg icon vue components, e.g. 
-  for (const [name, htmlString] of Object.entries(svgs)) {
-    const template = htmlString
-      .replace(/height="[0-9]+"/, 'v-bind:height="size"')
-      .replace(/width="[0-9]+"/, 'v-bind:width="size"');
-
-    app.component(name, {
-      props: {
-        size: {
-          type: String,
-          default: '16',
-        },
-      },
-      template,
-    });
-  }
-}
-
-export function initVueApp(el, opts = {}) {
-  if (typeof el === 'string') {
-    el = document.querySelector(el);
-  }
-  if (!el) return null;
-
-  return createApp(
-    {delimiters: vueDelimiters, ...opts}
-  ).mount(el);
-}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 6b4f4ef3e..480661118 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -2,9 +2,8 @@
 import './bootstrap.js';
 
 import $ from 'jquery';
-import {initVueEnv} from './components/VueComponentLoader.js';
 import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue';
-import {initDashboardRepoList} from './components/DashboardRepoList.js';
+import {initDashboardRepoList} from './components/DashboardRepoList.vue';
 
 import {attachTribute} from './features/tribute.js';
 import {initGlobalCopyToClipboardListener} from './features/clipboard.js';
@@ -100,7 +99,6 @@ $.fn.tab.settings.silent = true;
 // Disable the behavior of fomantic to toggle the checkbox when you press enter on a checkbox element.
 $.fn.checkbox.settings.enableEnterKey = false;
 
-initVueEnv();
 $(document).ready(() => {
   initGlobalCommon();
 
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 6476f16bf..9eabca3fd 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -31,8 +31,17 @@ import octiconSkip from '../../public/img/svg/octicon-skip.svg';
 import octiconMeter from '../../public/img/svg/octicon-meter.svg';
 import octiconBlocked from '../../public/img/svg/octicon-blocked.svg';
 import octiconSync from '../../public/img/svg/octicon-sync.svg';
+import octiconFilter from '../../public/img/svg/octicon-filter.svg';
+import octiconPlus from '../../public/img/svg/octicon-plus.svg';
+import octiconSearch from '../../public/img/svg/octicon-search.svg';
+import octiconArchive from '../../public/img/svg/octicon-archive.svg';
+import octiconStar from '../../public/img/svg/octicon-star.svg';
+import giteaDoubleChevronLeft from '../../public/img/svg/gitea-double-chevron-left.svg';
+import giteaDoubleChevronRight from '../../public/img/svg/gitea-double-chevron-right.svg';
+import octiconChevronLeft from '../../public/img/svg/octicon-chevron-left.svg';
+import octiconOrganization from '../../public/img/svg/octicon-organization.svg';
 
-export const svgs = {
+const svgs = {
   'octicon-blocked': octiconBlocked,
   'octicon-check-circle-fill': octiconCheckCircleFill,
   'octicon-chevron-down': octiconChevronDown,
@@ -66,14 +75,25 @@ export const svgs = {
   'octicon-triangle-down': octiconTriangleDown,
   'octicon-x': octiconX,
   'octicon-x-circle-fill': octiconXCircleFill,
+  'octicon-filter': octiconFilter,
+  'octicon-plus': octiconPlus,
+  'octicon-search': octiconSearch,
+  'octicon-archive': octiconArchive,
+  'octicon-star': octiconStar,
+  'gitea-double-chevron-left': giteaDoubleChevronLeft,
+  'gitea-double-chevron-right': giteaDoubleChevronRight,
+  'octicon-chevron-left': octiconChevronLeft,
+  'octicon-organization': octiconOrganization,
 };
 
+// TODO: use a more general approach to access SVG icons. At the moment, developers must check, pick and fill the names manually, most of the SVG icons in assets couldn't be used directly.
+
 const parser = new DOMParser();
 const serializer = new XMLSerializer();
 
-// retrieve a HTML string for given SVG icon name, size and additional classes
+// retrieve an HTML string for given SVG icon name, size and additional classes
 export function svg(name, size = 16, className = '') {
-  if (!(name in svgs)) return '';
+  if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`);
   if (size === 16 && !className) return svgs[name];
 
   const document = parser.parseFromString(svgs[name], 'image/svg+xml');