From 83e04328dfff3b09e5d28dd972ebee0865f96b0e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?=
 <72873130+zokkis@users.noreply.github.com>
Date: Sun, 3 Mar 2024 11:18:34 +0100
Subject: [PATCH] Filter Repositories by type (#29231)

Filter Repositories by type (resolves #1170, #1318)

before:

![image](https://github.com/go-gitea/gitea/assets/72873130/74e6be62-9010-4ab4-8f9b-bd8afbebb8fb)

after:

![image](https://github.com/go-gitea/gitea/assets/72873130/e4d85ed6-7864-4150-8d72-5194dac1293f)

(cherry picked from commit e3524c63d6d42865ea8288af89b372544d35474b)
---
 options/locale/locale_en-US.ini               | 13 ++++
 routers/web/explore/repo.go                   | 20 ++++++
 routers/web/org/home.go                       | 20 ++++++
 routers/web/user/notification.go              | 20 ++++++
 routers/web/user/profile.go                   | 30 +++++++++
 templates/admin/repo/list.tmpl                |  2 +-
 templates/admin/repo/search.tmpl              | 29 --------
 templates/explore/repo_search.tmpl            | 44 ------------
 templates/explore/repos.tmpl                  |  2 +-
 templates/org/home.tmpl                       |  2 +-
 templates/shared/repo_search.tmpl             | 67 +++++++++++++++++++
 .../notification_subscriptions.tmpl           |  2 +-
 templates/user/profile.tmpl                   |  4 +-
 web_src/js/features/repo-search.js            | 22 ++++++
 web_src/js/index.js                           |  2 +
 15 files changed, 200 insertions(+), 79 deletions(-)
 delete mode 100644 templates/admin/repo/search.tmpl
 delete mode 100644 templates/explore/repo_search.tmpl
 create mode 100644 templates/shared/repo_search.tmpl
 create mode 100644 web_src/js/features/repo-search.js

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index fd3aa8d6e1..07ad19aa7a 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -143,6 +143,19 @@ confirm_delete_selected = Confirm to delete all selected items?
 name = Name
 value = Value
 
+filter = Filter
+filter.clear = Clear Filter
+filter.is_archived = Archived
+filter.not_archived = Not Archived
+filter.is_fork = Forked
+filter.not_fork = Not Forked
+filter.is_mirror = Mirrored
+filter.not_mirror = Not Mirrored
+filter.is_template = Template
+filter.not_template = Not Template
+filter.public = Public
+filter.private = Private
+
 [aria]
 navbar = Navigation Bar
 footer = Footer
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index d5a46f6883..cf7381512b 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -109,6 +109,21 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
 	language := ctx.FormTrim("language")
 	ctx.Data["Language"] = language
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
 		ListOptions: db.ListOptions{
 			Page:     page,
@@ -125,6 +140,11 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
 		Language:           language,
 		IncludeDescription: setting.UI.SearchRepoDescription,
 		OnlyShowRelevant:   opts.OnlyShowRelevant,
+		Archived:           archived,
+		Fork:               fork,
+		Mirror:             mirror,
+		Template:           template,
+		IsPrivate:          private,
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 4a7378689a..71d10f3a43 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -85,6 +85,21 @@ func Home(ctx *context.Context) {
 		page = 1
 	}
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	var (
 		repos []*repo_model.Repository
 		count int64
@@ -102,6 +117,11 @@ func Home(ctx *context.Context) {
 		Actor:              ctx.Doer,
 		Language:           language,
 		IncludeDescription: setting.UI.SearchRepoDescription,
+		Archived:           archived,
+		Fork:               fork,
+		Mirror:             mirror,
+		Template:           template,
+		IsPrivate:          private,
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 09e592d63a..324205ed91 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -389,6 +389,21 @@ func NotificationWatching(ctx *context.Context) {
 		orderBy = db.SearchOrderByRecentUpdated
 	}
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
 		ListOptions: db.ListOptions{
 			PageSize: setting.UI.User.RepoPagingNum,
@@ -402,6 +417,11 @@ func NotificationWatching(ctx *context.Context) {
 		Collaborate:        optional.Some(false),
 		TopicOnly:          ctx.FormBool("topic"),
 		IncludeDescription: setting.UI.SearchRepoDescription,
+		Archived:           archived,
+		Fork:               fork,
+		Mirror:             mirror,
+		Template:           template,
+		IsPrivate:          private,
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 237363c19f..89d22d3ef4 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -164,6 +164,21 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 	}
 	ctx.Data["NumFollowing"] = numFollowing
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	switch tab {
 	case "followers":
 		ctx.Data["Cards"] = followers
@@ -210,6 +225,11 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
+			Archived:           archived,
+			Fork:               fork,
+			Mirror:             mirror,
+			Template:           template,
+			IsPrivate:          private,
 		})
 		if err != nil {
 			ctx.ServerError("SearchRepository", err)
@@ -232,6 +252,11 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
+			Archived:           archived,
+			Fork:               fork,
+			Mirror:             mirror,
+			Template:           template,
+			IsPrivate:          private,
 		})
 		if err != nil {
 			ctx.ServerError("SearchRepository", err)
@@ -277,6 +302,11 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
+			Archived:           archived,
+			Fork:               fork,
+			Mirror:             mirror,
+			Template:           template,
+			IsPrivate:          private,
 		})
 		if err != nil {
 			ctx.ServerError("SearchRepository", err)
diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl
index e11247aed4..e977c8307c 100644
--- a/templates/admin/repo/list.tmpl
+++ b/templates/admin/repo/list.tmpl
@@ -7,7 +7,7 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			{{template "admin/repo/search" .}}
+			{{template "shared/repo_search" .}}
 		</div>
 		<div class="ui attached table segment">
 			<table class="ui very basic striped table unstackable">
diff --git a/templates/admin/repo/search.tmpl b/templates/admin/repo/search.tmpl
deleted file mode 100644
index 247ec5491a..0000000000
--- a/templates/admin/repo/search.tmpl
+++ /dev/null
@@ -1,29 +0,0 @@
-<div class="ui secondary filter menu gt-ac gt-mx-0">
-	<form class="ui form ignore-dirty gt-f1">
-		<div class="ui fluid action input">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-		</div>
-	</form>
-	<!-- Sort -->
-	<div class="ui dropdown type jump item gt-mr-0">
-		<span class="text">
-			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
-		</span>
-		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-		<div class="menu">
-			<a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-			<a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?sort=moststars&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a>
-			<a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?sort=feweststars&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a>
-			<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?sort=mostforks&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
-			<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?sort=fewestforks&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
-			<a class="{{if eq .SortType "size"}}active {{end}}item" href="{{$.Link}}?sort=size&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.by_size"}}</a>
-			<a class="{{if eq .SortType "reversesize"}}active {{end}}item" href="{{$.Link}}?sort=reversesize&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size"}}</a>
-		</div>
-	</div>
-</div>
diff --git a/templates/explore/repo_search.tmpl b/templates/explore/repo_search.tmpl
deleted file mode 100644
index 1fd891cca2..0000000000
--- a/templates/explore/repo_search.tmpl
+++ /dev/null
@@ -1,44 +0,0 @@
-<div class="ui secondary filter menu gt-ac gt-mx-0">
-	<form class="ui form ignore-dirty gt-f1">
-		<input type="hidden" name="sort" value="{{$.SortType}}">
-		<input type="hidden" name="language" value="{{$.Language}}">
-		<div class="ui fluid action input">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			{{if .PageIsExploreRepositories}}
-				<input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}">
-			{{else if .TabName}}
-				<input type="hidden" name="tab" value="{{.TabName}}">
-			{{end}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-		</div>
-	</form>
-	<!-- Sort -->
-	<div class="ui dropdown type jump item gt-mr-0">
-		<span class="text">
-			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
-		</span>
-		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-		<div class="menu">
-			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=newest&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=oldest&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=alphabetically&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=reversealphabetically&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=recentupdate&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=leastupdate&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-			{{if not .DisableStars}}
-				<a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=moststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a>
-				<a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=feweststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a>
-			{{end}}
-			{{if not .DisableForks}}
-				<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=mostforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
-				<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=fewestforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
-			{{end}}
-		</div>
-	</div>
-</div>
-{{if and .PageIsExploreRepositories .OnlyShowRelevant}}
-	<div class="ui message explore-relevancy-note">
-		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}}</span>
-	</div>
-{{end}}
-<div class="divider"></div>
diff --git a/templates/explore/repos.tmpl b/templates/explore/repos.tmpl
index dfede2ffcc..53742bf0d9 100644
--- a/templates/explore/repos.tmpl
+++ b/templates/explore/repos.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content explore repositories">
 	{{template "explore/navbar" .}}
 	<div class="ui container">
-		{{template "explore/repo_search" .}}
+		{{template "shared/repo_search" .}}
 		{{template "explore/repo_list" .}}
 		{{template "base/paginate" .}}
 	</div>
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index 9deacce139..d0a39c04fc 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -13,7 +13,7 @@
 				{{if .ProfileReadme}}
 					<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
 				{{end}}
-				{{template "explore/repo_search" .}}
+				{{template "shared/repo_search" .}}
 				{{template "explore/repo_list" .}}
 				{{template "base/paginate" .}}
 			</div>
diff --git a/templates/shared/repo_search.tmpl b/templates/shared/repo_search.tmpl
new file mode 100644
index 0000000000..2ea4bfaad7
--- /dev/null
+++ b/templates/shared/repo_search.tmpl
@@ -0,0 +1,67 @@
+<div class="ui secondary filter menu">
+	<form id="repo-search-form" class="ui form ignore-dirty tw-flex-1 tw-flex tw-flex-row tw-gap-x-2">
+		{{if .Language}}<input hidden name="language" value="{{.Language}}">{{end}}
+		<div class="ui fluid action input tw-flex-1">
+			{{template "shared/searchinput" dict "Value" .Keyword}}
+			{{if .PageIsExploreRepositories}}
+				<input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}">
+			{{else if .TabName}}
+				<input type="hidden" name="tab" value="{{.TabName}}">
+			{{end}}
+			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+		</div>
+		<!-- Filter -->
+		<div class="ui dropdown type jump item tw-mr-0">
+			<span class="text">
+				{{ctx.Locale.Tr "filter"}}
+			</span>
+			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+			<div class="menu">
+				<label class="item"><input type="radio" name="clear-filter"> {{ctx.Locale.Tr "filter.clear"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="archived" {{if .IsArchived.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_archived"}}</label>
+				<label class="item"><input type="radio" name="archived" {{if (not (.IsArchived.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_archived"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="fork" {{if .IsFork.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_fork"}}</label>
+				<label class="item"><input type="radio" name="fork" {{if (not (.IsFork.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_fork"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="mirror" {{if .IsMirror.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_mirror"}}</label>
+				<label class="item"><input type="radio" name="mirror" {{if (not (.IsMirror.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_mirror"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="template" {{if .IsTemplate.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_template"}}</label>
+				<label class="item"><input type="radio" name="template" {{if (not (.IsTemplate.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_template"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="private" {{if .IsPrivate.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.private"}}</label>
+				<label class="item"><input type="radio" name="private" {{if (not (.IsPrivate.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.public"}}</label>
+			</div>
+		</div>
+		<!-- Sort -->
+		<div class="ui dropdown type jump item gt-mr-0">
+			<span class="text">
+				{{ctx.Locale.Tr "repo.issues.filter_sort"}}
+			</span>
+			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+			<div class="menu">
+				<label class="{{if eq .SortType "newest"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "newest"}}checked{{end}} value="newest"> {{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</label>
+				<label class="{{if eq .SortType "oldest"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "oldest"}}checked{{end}} value="oldest"> {{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</label>
+				<label class="{{if eq .SortType "alphabetically"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "alphabetically"}}checked{{end}} value="alphabetically"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</label>
+				<label class="{{if eq .SortType "reversealphabetically"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "reversealphabetically"}}checked{{end}} value="reversealphabetically"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</label>
+				<label class="{{if eq .SortType "recentupdate"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "recentupdate"}}checked{{end}} value="recentupdate"> {{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</label>
+				<label class="{{if eq .SortType "leastupdate"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "leastupdate"}}checked{{end}} value="leastupdate"> {{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</label>
+				{{if not .DisableStars}}
+					<label class="{{if eq .SortType "moststars"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "moststars"}}checked{{end}} value="moststars"> {{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</label>
+					<label class="{{if eq .SortType "feweststars"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "feweststars"}}checked{{end}} value="feweststars"> {{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</label>
+				{{end}}
+				<label class="{{if eq .SortType "mostforks"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "mostforks"}}checked{{end}} value="mostforks"> {{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</label>
+				<label class="{{if eq .SortType "fewestforks"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "fewestforks"}}checked{{end}} value="fewestforks"> {{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</label>
+				<label class="{{if eq .SortType "size"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "size"}}checked{{end}} value="size"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.by_size"}}</label>
+				<label class="{{if eq .SortType "reversesize"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "reversesize"}}checked{{end}} value="reversesize"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size"}}</label>
+			</div>
+		</div>
+	</form>
+</div>
+{{if and .PageIsExploreRepositories .OnlyShowRelevant}}
+	<div class="ui message explore-relevancy-note">
+		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}}</span>
+	</div>
+{{end}}
diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl
index ec40d3afea..a37f0c352e 100644
--- a/templates/user/notification/notification_subscriptions.tmpl
+++ b/templates/user/notification/notification_subscriptions.tmpl
@@ -69,7 +69,7 @@
 					{{template "shared/issuelist" dict "." . "listType" "dashboard"}}
 				{{end}}
 			{{else}}
-				{{template "explore/repo_search" .}}
+				{{template "shared/repo_search" .}}
 				{{template "explore/repo_list" .}}
 				{{template "base/paginate" .}}
 			{{end}}
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index 079c6679b7..da68d50ab9 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -21,7 +21,7 @@
 					{{template "user/dashboard/feeds" .}}
 				{{else if eq .TabName "stars"}}
 					<div class="stars">
-						{{template "explore/repo_search" .}}
+						{{template "shared/repo_search" .}}
 						{{template "explore/repo_list" .}}
 						{{template "base/paginate" .}}
 					</div>
@@ -32,7 +32,7 @@
 				{{else if eq .TabName "overview"}}
 					<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
 				{{else}}
-					{{template "explore/repo_search" .}}
+					{{template "shared/repo_search" .}}
 					{{template "explore/repo_list" .}}
 					{{template "base/paginate" .}}
 				{{end}}
diff --git a/web_src/js/features/repo-search.js b/web_src/js/features/repo-search.js
new file mode 100644
index 0000000000..185f6119d9
--- /dev/null
+++ b/web_src/js/features/repo-search.js
@@ -0,0 +1,22 @@
+export function initRepositorySearch() {
+  const repositorySearchForm = document.querySelector('#repo-search-form');
+  if (!repositorySearchForm) return;
+
+  repositorySearchForm.addEventListener('change', (e) => {
+    e.preventDefault();
+
+    const formData = new FormData(repositorySearchForm);
+    const params = new URLSearchParams(formData);
+
+    if (e.target.name === 'clear-filter') {
+      params.delete('archived');
+      params.delete('fork');
+      params.delete('mirror');
+      params.delete('template');
+      params.delete('private');
+    }
+
+    params.delete('clear-filter');
+    window.location.search = params.toString();
+  });
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index c7eac9d242..abf0d469d1 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -84,6 +84,7 @@ import {initRepoCodeFrequency} from './features/code-frequency.js';
 import {initRepoRecentCommits} from './features/recent-commits.js';
 import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
 import {initDirAuto} from './modules/dirauto.js';
+import {initRepositorySearch} from './features/repo-search.js';
 
 // Init Gitea's Fomantic settings
 initGiteaFomantic();
@@ -170,6 +171,7 @@ onDomReady(() => {
   initRepoWikiForm();
   initRepository();
   initRepositoryActionView();
+  initRepositorySearch();
   initRepoContributors();
   initRepoCodeFrequency();
   initRepoRecentCommits();