diff --git a/modules/pages/config.go b/modules/pages/config.go index db8aac463e..018b1132fd 100644 --- a/modules/pages/config.go +++ b/modules/pages/config.go @@ -83,13 +83,22 @@ type BrandConfig struct { // HeroConfig represents hero section settings type HeroConfig struct { - Headline string `yaml:"headline,omitempty"` - Subheadline string `yaml:"subheadline,omitempty"` - PrimaryCTA CTAButton `yaml:"primary_cta,omitempty"` - SecondaryCTA CTAButton `yaml:"secondary_cta,omitempty"` - ImageURL string `yaml:"image_url,omitempty"` - CodeExample string `yaml:"code_example,omitempty"` - VideoURL string `yaml:"video_url,omitempty"` + Headline string `yaml:"headline,omitempty"` + Subheadline string `yaml:"subheadline,omitempty"` + PrimaryCTA CTAButton `yaml:"primary_cta,omitempty"` + SecondaryCTA CTAButton `yaml:"secondary_cta,omitempty"` + ImageURL string `yaml:"image_url,omitempty"` + UploadedImage string `yaml:"uploaded_image,omitempty"` // filename in repo-avatars storage + CodeExample string `yaml:"code_example,omitempty"` + VideoURL string `yaml:"video_url,omitempty"` +} + +// ResolvedImageURL returns the effective hero image URL, preferring uploaded image over URL. +func (h *HeroConfig) ResolvedImageURL() string { + if h.UploadedImage != "" { + return "/repo-avatars/" + h.UploadedImage + } + return h.ImageURL } // CTAButton represents a call-to-action button diff --git a/options/locale/custom_keys.json b/options/locale/custom_keys.json index 0cc508a0e2..70e2abf16d 100644 --- a/options/locale/custom_keys.json +++ b/options/locale/custom_keys.json @@ -697,6 +697,17 @@ "repo.settings.pages.brand_tagline": "Tagline", "repo.settings.pages.headline": "Headline", "repo.settings.pages.subheadline": "Subheadline", + "repo.settings.pages.hero_image": "Hero Image", + "repo.settings.pages.hero_upload": "Upload Image", + "repo.settings.pages.hero_upload_btn": "Upload", + "repo.settings.pages.hero_upload_help": "Upload a JPG, PNG, WebP, or GIF image (max 5 MB). Uploaded images take priority over the URL field.", + "repo.settings.pages.hero_upload_error": "Failed to upload image.", + "repo.settings.pages.hero_image_too_large": "Image is too large. Maximum size is 5 MB.", + "repo.settings.pages.hero_not_an_image": "The uploaded file is not a valid image.", + "repo.settings.pages.hero_image_uploaded": "Hero image uploaded successfully.", + "repo.settings.pages.hero_image_deleted": "Hero image deleted.", + "repo.settings.pages.hero_delete_image": "Delete Image", + "repo.settings.pages.hero_or": "or use a URL", "repo.settings.pages.image_url": "Hero Image URL", "repo.settings.pages.video_url": "Demo Video URL", "repo.settings.pages.code_example": "Code Example", diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 351d302fe0..4d83c12a76 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -4464,6 +4464,17 @@ "repo.settings.pages.brand_tagline": "Tagline", "repo.settings.pages.headline": "Headline", "repo.settings.pages.subheadline": "Subheadline", + "repo.settings.pages.hero_image": "Hero Image", + "repo.settings.pages.hero_upload": "Upload Image", + "repo.settings.pages.hero_upload_btn": "Upload", + "repo.settings.pages.hero_upload_help": "Upload a JPG, PNG, WebP, or GIF image (max 5 MB). Uploaded images take priority over the URL field.", + "repo.settings.pages.hero_upload_error": "Failed to upload image.", + "repo.settings.pages.hero_image_too_large": "Image is too large. Maximum size is 5 MB.", + "repo.settings.pages.hero_not_an_image": "The uploaded file is not a valid image.", + "repo.settings.pages.hero_image_uploaded": "Hero image uploaded successfully.", + "repo.settings.pages.hero_image_deleted": "Hero image deleted.", + "repo.settings.pages.hero_delete_image": "Delete Image", + "repo.settings.pages.hero_or": "or use a URL", "repo.settings.pages.image_url": "Hero Image URL", "repo.settings.pages.video_url": "Demo Video URL", "repo.settings.pages.code_example": "Code Example", diff --git a/routers/web/repo/setting/pages.go b/routers/web/repo/setting/pages.go index 5fbb00cbd9..675779647d 100644 --- a/routers/web/repo/setting/pages.go +++ b/routers/web/repo/setting/pages.go @@ -4,7 +4,9 @@ package setting import ( + "crypto/sha256" "fmt" + "io" "net/http" "slices" "strings" @@ -16,7 +18,9 @@ import ( "code.gitcaddy.com/server/v3/modules/json" "code.gitcaddy.com/server/v3/modules/log" pages_module "code.gitcaddy.com/server/v3/modules/pages" + "code.gitcaddy.com/server/v3/modules/storage" "code.gitcaddy.com/server/v3/modules/templates" + "code.gitcaddy.com/server/v3/modules/typesniffer" "code.gitcaddy.com/server/v3/services/context" pages_service "code.gitcaddy.com/server/v3/services/pages" ) @@ -282,6 +286,85 @@ func PagesHeroPost(ctx *context.Context) { ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/hero") } +const maxHeroImageSize = 5 * 1024 * 1024 // 5MB + +// PagesHeroUploadImage handles hero image file upload. +func PagesHeroUploadImage(ctx *context.Context) { + repo := ctx.Repo.Repository + + file, header, err := ctx.Req.FormFile("hero_image") + if err != nil { + ctx.Flash.Error(ctx.Tr("repo.settings.pages.hero_upload_error")) + ctx.Redirect(repo.Link() + "/settings/pages/hero") + return + } + defer file.Close() + + if header.Size > maxHeroImageSize { + ctx.Flash.Error(ctx.Tr("repo.settings.pages.hero_image_too_large")) + ctx.Redirect(repo.Link() + "/settings/pages/hero") + return + } + + data, err := io.ReadAll(file) + if err != nil { + ctx.ServerError("ReadAll", err) + return + } + + st := typesniffer.DetectContentType(data) + if !(st.IsImage() && !st.IsSvgImage()) { + ctx.Flash.Error(ctx.Tr("repo.settings.pages.hero_not_an_image")) + ctx.Redirect(repo.Link() + "/settings/pages/hero") + return + } + + hash := sha256.Sum256(data) + filename := fmt.Sprintf("pages-hero-%d-%x", repo.ID, hash[:8]) + + config := getPagesLandingConfig(ctx) + + // Delete old uploaded image if it exists + if config.Hero.UploadedImage != "" { + _ = storage.RepoAvatars.Delete(config.Hero.UploadedImage) + } + + if err := storage.SaveFrom(storage.RepoAvatars, filename, func(w io.Writer) error { + _, err := w.Write(data) + return err + }); err != nil { + ctx.ServerError("SaveFrom", err) + return + } + + config.Hero.UploadedImage = filename + if err := savePagesLandingConfig(ctx, config); err != nil { + ctx.ServerError("SavePagesConfig", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.pages.hero_image_uploaded")) + ctx.Redirect(repo.Link() + "/settings/pages/hero") +} + +// PagesHeroDeleteImage removes the uploaded hero image. +func PagesHeroDeleteImage(ctx *context.Context) { + repo := ctx.Repo.Repository + config := getPagesLandingConfig(ctx) + + if config.Hero.UploadedImage != "" { + _ = storage.RepoAvatars.Delete(config.Hero.UploadedImage) + config.Hero.UploadedImage = "" + if err := savePagesLandingConfig(ctx, config); err != nil { + ctx.ServerError("SavePagesConfig", err) + return + } + } + + ctx.Flash.Success(ctx.Tr("repo.settings.pages.hero_image_deleted")) + ctx.Redirect(repo.Link() + "/settings/pages/hero") +} + func PagesContent(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.pages.content") ctx.Data["PageIsSettingsPages"] = true diff --git a/routers/web/web.go b/routers/web/web.go index dcf3d98319..5ce2533439 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1334,6 +1334,8 @@ func registerWebRoutes(m *web.Router) { m.Combo("").Get(repo_setting.Pages).Post(repo_setting.PagesPost) m.Combo("/brand").Get(repo_setting.PagesBrand).Post(repo_setting.PagesBrandPost) m.Combo("/hero").Get(repo_setting.PagesHero).Post(repo_setting.PagesHeroPost) + m.Post("/hero/upload_image", repo_setting.PagesHeroUploadImage) + m.Post("/hero/delete_image", repo_setting.PagesHeroDeleteImage) m.Combo("/content").Get(repo_setting.PagesContent).Post(repo_setting.PagesContentPost) m.Combo("/social").Get(repo_setting.PagesSocial).Post(repo_setting.PagesSocialPost) m.Combo("/pricing").Get(repo_setting.PagesPricing).Post(repo_setting.PagesPricingPost) diff --git a/templates/repo/settings/pages_hero.tmpl b/templates/repo/settings/pages_hero.tmpl index 5ba98f6f8b..8da9bd23da 100644 --- a/templates/repo/settings/pages_hero.tmpl +++ b/templates/repo/settings/pages_hero.tmpl @@ -12,10 +12,38 @@ +
{{ctx.Locale.Tr "repo.settings.pages.hero_upload_help"}}
+