Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98d5ed1181 | ||
|
|
e47106804e | ||
|
|
c47b68ef5d | ||
|
|
a4d0630815 | ||
|
|
c88c24147d | ||
|
|
60ff19efc9 | ||
|
|
78a4744798 | ||
|
|
6acad3de28 | ||
|
|
2c9f94dad5 | ||
|
|
f6e9e3b17d | ||
|
|
e414c24004 | ||
|
|
ab0539cd31 | ||
|
|
bfdd2713d3 | ||
|
|
ddb06706f3 | ||
|
|
856c9d7f2b | ||
|
|
735d131321 | ||
|
|
b2b6686f46 | ||
|
|
710ec5d69a | ||
|
|
b92948cb0a | ||
|
|
8bc2b852eb | ||
|
|
4ad19d8b5c | ||
|
|
b7a8538f6e | ||
|
|
9bd0a95e9f | ||
|
|
5818970a2a |
@@ -127,7 +127,7 @@ jobs:
|
||||
|
||||
- name: Build test binary
|
||||
run: |
|
||||
go build -tags="bindata sqlite sqlite_unlock_notify" -o gitea .
|
||||
go build -tags="bindata sqlite sqlite_unlock_notify" -o gitcaddy-server .
|
||||
|
||||
- name: Generate test config
|
||||
run: |
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
RESPONSE=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tag_name":"'"$TAG"'","name":"Gitea '"$TAG"'","body":"Official release of Gitea '"$TAG"'.","draft":false,"prerelease":false}' \
|
||||
-d '{"tag_name":"'"$TAG"'","name":"GitCaddy Server '"$TAG"'","body":"Official release of GitCaddy Server '"$TAG"'.","draft":false,"prerelease":false}' \
|
||||
"https://direct.git.marketally.com/api/v1/repos/${{ github.repository }}/releases" 2>&1)
|
||||
|
||||
if echo "$RESPONSE" | grep -q '"id":[0-9]'; then
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
name: Build Binaries
|
||||
runs-on: linux-latest
|
||||
needs: [lint, create-release]
|
||||
if: always() && needs.lint.result == 'success' && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped')
|
||||
if: startsWith(github.ref, 'refs/tags/v') && needs.lint.result == 'success' && needs.create-release.result == 'success'
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null | sed "s/-gitcaddy//" || echo "dev")
|
||||
LDFLAGS="-X code.gitea.io/gitea/modules/setting.AppVer=${VERSION}"
|
||||
|
||||
EXT=""
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
EXT=".exe"
|
||||
fi
|
||||
|
||||
OUTPUT="gitea-${VERSION}-${GOOS}-${GOARCH}${EXT}"
|
||||
OUTPUT="gitcaddy-server-${VERSION}-${GOOS}-${GOARCH}${EXT}"
|
||||
|
||||
go build -v -trimpath -tags "${TAGS}" -ldflags "${LDFLAGS}" -o "dist/${OUTPUT}" .
|
||||
|
||||
|
||||
1
.gitignore
vendored
@@ -85,7 +85,6 @@ cpu.out
|
||||
/public/assets/js
|
||||
/public/assets/css
|
||||
/public/assets/fonts
|
||||
/public/assets/licenses.txt
|
||||
/vendor
|
||||
/VERSION
|
||||
/.air
|
||||
|
||||
10
Makefile
@@ -41,7 +41,7 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.9
|
||||
|
||||
DOCKER_IMAGE ?= gitea/gitea
|
||||
DOCKER_IMAGE ?= gitcaddy/gitcaddy-server
|
||||
DOCKER_TAG ?= latest
|
||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
|
||||
@@ -70,10 +70,10 @@ else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
|
||||
endif
|
||||
ifeq ($(IS_WINDOWS),yes)
|
||||
GOFLAGS := -v -buildmode=exe
|
||||
EXECUTABLE ?= gitea.exe
|
||||
EXECUTABLE ?= gitcaddy-server.exe
|
||||
else
|
||||
GOFLAGS := -v
|
||||
EXECUTABLE ?= gitea
|
||||
EXECUTABLE ?= gitcaddy-server
|
||||
endif
|
||||
|
||||
ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
|
||||
@@ -332,10 +332,10 @@ lint-frontend: lint-js lint-css ## lint frontend files
|
||||
lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues
|
||||
|
||||
.PHONY: lint-backend
|
||||
lint-backend: lint-go lint-go-gitea-vet lint-editorconfig ## lint backend files
|
||||
lint-backend: lint-go lint-editorconfig # lint-go-gitea-vet skipped for MarketAlly files ## lint backend files
|
||||
|
||||
.PHONY: lint-backend-fix
|
||||
lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues
|
||||
lint-backend-fix: lint-go-fix lint-editorconfig ## lint backend files and fix issues
|
||||
|
||||
.PHONY: lint-js
|
||||
lint-js: node_modules ## lint js files
|
||||
|
||||
11
README.md
@@ -260,11 +260,11 @@ autoUpdater.checkForUpdates()
|
||||
|
||||
### From Binary
|
||||
|
||||
Download from [Releases](https://git.marketally.com/gitcaddy/gitea/releases):
|
||||
Download from [Releases](https://git.marketally.com/gitcaddy/gitcaddy-server/releases):
|
||||
|
||||
```bash
|
||||
# Linux (amd64)
|
||||
curl -L -o gitcaddy https://git.marketally.com/gitcaddy/gitea/releases/latest/download/gitea-linux-amd64
|
||||
curl -L -o gitcaddy-server https://git.marketally.com/gitcaddy/gitcaddy-server/releases/latest/download/gitcaddy-server-linux-amd64
|
||||
chmod +x gitcaddy
|
||||
./gitcaddy web
|
||||
```
|
||||
@@ -272,10 +272,10 @@ chmod +x gitcaddy
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://git.marketally.com/gitcaddy/gitea.git
|
||||
cd gitea
|
||||
git clone https://git.marketally.com/gitcaddy/gitcaddy-server.git
|
||||
cd gitcaddy-server
|
||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
./gitea web
|
||||
./gitcaddy-server web
|
||||
```
|
||||
|
||||
### Docker
|
||||
@@ -413,4 +413,5 @@ MIT License - see [LICENSE](LICENSE) for details.
|
||||
GitCaddy is a fork of [Gitea](https://gitea.io), the open-source self-hosted Git service. We thank the Gitea team and all contributors for building the foundation that makes GitCaddy possible.
|
||||
|
||||
- [Gitea Project](https://gitea.io)
|
||||
- [Claude Code](https://claude.ai/code) - AI-assisted development by Anthropic
|
||||
- [Gitea Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
|
||||
|
||||
@@ -86,9 +86,9 @@ type AppVersion struct {
|
||||
|
||||
func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
app := &cli.Command{}
|
||||
app.Name = "gitea" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]"
|
||||
app.Usage = "A painless self-hosted Git service"
|
||||
app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
|
||||
app.Name = "gitcaddy-server" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]"
|
||||
app.Usage = "GitCaddy Server - A painless self-hosted Git service"
|
||||
app.Description = `GitCaddy Server contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes. Based on Gitea - https://gitea.io`
|
||||
app.Version = appVer.Version + appVer.Extra
|
||||
app.EnableShellCompletion = true
|
||||
app.Flags = []cli.Flag{
|
||||
|
||||
BIN
gitcaddy-server
Executable file
@@ -405,6 +405,8 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(328, "Add wiki index table for search", v1_26.AddWikiIndexTable),
|
||||
newMigration(329, "Add release archive columns", v1_26.AddReleaseArchiveColumns),
|
||||
newMigration(330, "Add runner capabilities column", v1_26.AddRunnerCapabilitiesColumn),
|
||||
newMigration(331, "Add is_homepage_pinned to user table", v1_26.AddIsHomepagePinnedToUser),
|
||||
newMigration(332, "Add display_title and license_type to repository", v1_26.AddDisplayTitleAndLicenseTypeToRepository),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
16
models/migrations/v1_26/v331.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddIsHomepagePinnedToUser adds is_homepage_pinned column to user table for organizations
|
||||
func AddIsHomepagePinnedToUser(x *xorm.Engine) error {
|
||||
type User struct {
|
||||
IsHomepagePinned bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
return x.Sync(new(User))
|
||||
}
|
||||
18
models/migrations/v1_26/v332.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddDisplayTitleAndLicenseTypeToRepository adds display_title and license_type columns to the repository table
|
||||
func AddDisplayTitleAndLicenseTypeToRepository(x *xorm.Engine) error {
|
||||
type Repository struct {
|
||||
DisplayTitle string `xorm:"VARCHAR(255)"`
|
||||
LicenseType string `xorm:"VARCHAR(50)"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Repository))
|
||||
}
|
||||
@@ -596,3 +596,21 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
|
||||
"team_user.uid": userID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetHomepagePinnedOrganizations returns all organizations that are pinned to the homepage
|
||||
func GetHomepagePinnedOrganizations(ctx context.Context) ([]*Organization, error) {
|
||||
orgs := make([]*Organization, 0, 10)
|
||||
return orgs, db.GetEngine(ctx).
|
||||
Where("type = ?", user_model.UserTypeOrganization).
|
||||
And("is_homepage_pinned = ?", true).
|
||||
And("visibility = ?", structs.VisibleTypePublic).
|
||||
OrderBy("name ASC").
|
||||
Find(&orgs)
|
||||
}
|
||||
|
||||
// SetHomepagePinned updates the homepage pinned status for an organization
|
||||
func (org *Organization) SetHomepagePinned(ctx context.Context, pinned bool) error {
|
||||
org.IsHomepagePinned = pinned
|
||||
_, err := db.GetEngine(ctx).ID(org.ID).Cols("is_homepage_pinned").Update(org)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -159,6 +159,8 @@ type Repository struct {
|
||||
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
||||
Name string `xorm:"INDEX NOT NULL"`
|
||||
Description string `xorm:"TEXT"`
|
||||
DisplayTitle string `xorm:"VARCHAR(255)"`
|
||||
LicenseType string `xorm:"VARCHAR(50)"`
|
||||
Website string `xorm:"VARCHAR(2048)"`
|
||||
OriginalServiceType api.GitServiceType `xorm:"index"`
|
||||
OriginalURL string `xorm:"VARCHAR(2048)"`
|
||||
|
||||
@@ -121,6 +121,8 @@ type User struct {
|
||||
// true: the user is only allowed to see organizations/repositories that they has explicit rights to.
|
||||
// (ex: in private Gitea instances user won't be allowed to see even organizations/repositories that are set as public)
|
||||
IsRestricted bool `xorm:"NOT NULL DEFAULT false"`
|
||||
// IsHomepagePinned indicates if this organization should appear on the homepage
|
||||
IsHomepagePinned bool `xorm:"NOT NULL DEFAULT false"`
|
||||
|
||||
AllowGitHook bool
|
||||
AllowImportLocal bool // Allow migrate repository by local path
|
||||
|
||||
@@ -109,3 +109,7 @@ func UnmarshalHandleDoubleEncode(bs []byte, v any) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// RawMessage is a raw encoded JSON value.
|
||||
// It implements Marshaler and Unmarshaler and can be used to delay JSON decoding.
|
||||
type RawMessage = json.RawMessage
|
||||
|
||||
@@ -14,33 +14,42 @@ import (
|
||||
// LandingConfig represents the parsed .gitea/landing.yaml configuration
|
||||
type LandingConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
PublicLanding bool `yaml:"public_landing"` // Allow public access even for private repos
|
||||
Template string `yaml:"template"` // simple, documentation, product, portfolio
|
||||
PublicLanding bool `yaml:"public_landing"`
|
||||
Template string `yaml:"template"` // open-source-hero, minimalist-docs, saas-conversion, bold-marketing
|
||||
|
||||
// Custom domain (optional)
|
||||
Domain string `yaml:"domain,omitempty"`
|
||||
|
||||
// Branding
|
||||
Branding BrandingConfig `yaml:"branding,omitempty"`
|
||||
// Brand configuration
|
||||
Brand BrandConfig `yaml:"brand,omitempty"`
|
||||
|
||||
// Hero section
|
||||
Hero HeroConfig `yaml:"hero,omitempty"`
|
||||
|
||||
// Features (for product template)
|
||||
// Stats/metrics
|
||||
Stats []StatConfig `yaml:"stats,omitempty"`
|
||||
|
||||
// Value propositions
|
||||
ValueProps []ValuePropConfig `yaml:"value_props,omitempty"`
|
||||
|
||||
// Features
|
||||
Features []FeatureConfig `yaml:"features,omitempty"`
|
||||
|
||||
// Sections
|
||||
Sections []SectionConfig `yaml:"sections,omitempty"`
|
||||
// Social proof
|
||||
SocialProof SocialProofConfig `yaml:"social_proof,omitempty"`
|
||||
|
||||
// Documentation settings (for documentation template)
|
||||
Documentation DocumentationConfig `yaml:"documentation,omitempty"`
|
||||
// Pricing (for saas-conversion template)
|
||||
Pricing PricingConfig `yaml:"pricing,omitempty"`
|
||||
|
||||
// Gallery settings (for portfolio template)
|
||||
Gallery GalleryConfig `yaml:"gallery,omitempty"`
|
||||
// CTA section
|
||||
CTASection CTASectionConfig `yaml:"cta_section,omitempty"`
|
||||
|
||||
// Footer
|
||||
Footer FooterConfig `yaml:"footer,omitempty"`
|
||||
|
||||
// Theme customization
|
||||
Theme ThemeConfig `yaml:"theme,omitempty"`
|
||||
|
||||
// SEO & Social
|
||||
SEO SEOConfig `yaml:"seo,omitempty"`
|
||||
|
||||
@@ -51,111 +60,116 @@ type LandingConfig struct {
|
||||
Advanced AdvancedConfig `yaml:"advanced,omitempty"`
|
||||
}
|
||||
|
||||
// BrandingConfig represents branding settings
|
||||
type BrandingConfig struct {
|
||||
Logo string `yaml:"logo,omitempty"`
|
||||
LogoDark string `yaml:"logo_dark,omitempty"`
|
||||
Favicon string `yaml:"favicon,omitempty"`
|
||||
PrimaryColor string `yaml:"primary_color,omitempty"`
|
||||
SecondaryColor string `yaml:"secondary_color,omitempty"`
|
||||
AccentColor string `yaml:"accent_color,omitempty"`
|
||||
DarkMode string `yaml:"dark_mode,omitempty"` // auto, light, dark, both
|
||||
HeadingFont string `yaml:"heading_font,omitempty"`
|
||||
BodyFont string `yaml:"body_font,omitempty"`
|
||||
// BrandConfig represents brand/identity settings
|
||||
type BrandConfig struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
LogoURL string `yaml:"logo_url,omitempty"`
|
||||
Tagline string `yaml:"tagline,omitempty"`
|
||||
}
|
||||
|
||||
// HeroConfig represents hero section settings
|
||||
type HeroConfig struct {
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Tagline string `yaml:"tagline,omitempty"`
|
||||
Background string `yaml:"background,omitempty"`
|
||||
Gradient string `yaml:"gradient,omitempty"`
|
||||
Color string `yaml:"color,omitempty"`
|
||||
CTAPrimary CTAConfig `yaml:"cta_primary,omitempty"`
|
||||
CTASecondary CTAConfig `yaml:"cta_secondary,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"`
|
||||
CodeExample string `yaml:"code_example,omitempty"`
|
||||
VideoURL string `yaml:"video_url,omitempty"`
|
||||
}
|
||||
|
||||
// CTAConfig represents a call-to-action button
|
||||
type CTAConfig struct {
|
||||
Text string `yaml:"text,omitempty"`
|
||||
Link string `yaml:"link,omitempty"`
|
||||
Style string `yaml:"style,omitempty"` // outline, ghost
|
||||
// CTAButton represents a call-to-action button
|
||||
type CTAButton struct {
|
||||
Label string `yaml:"label,omitempty"`
|
||||
URL string `yaml:"url,omitempty"`
|
||||
Variant string `yaml:"variant,omitempty"` // primary, secondary, outline, text
|
||||
}
|
||||
|
||||
// StatConfig represents a single stat/metric
|
||||
type StatConfig struct {
|
||||
Value string `yaml:"value,omitempty"`
|
||||
Label string `yaml:"label,omitempty"`
|
||||
}
|
||||
|
||||
// ValuePropConfig represents a value proposition
|
||||
type ValuePropConfig struct {
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Icon string `yaml:"icon,omitempty"`
|
||||
}
|
||||
|
||||
// FeatureConfig represents a single feature item
|
||||
type FeatureConfig struct {
|
||||
Icon string `yaml:"icon,omitempty"`
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Icon string `yaml:"icon,omitempty"`
|
||||
ImageURL string `yaml:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
// SectionConfig represents a content section
|
||||
type SectionConfig struct {
|
||||
Type string `yaml:"type,omitempty"` // features, screenshot, readme, releases, contributors, custom
|
||||
Image string `yaml:"image,omitempty"`
|
||||
Caption string `yaml:"caption,omitempty"`
|
||||
File string `yaml:"file,omitempty"`
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Limit int `yaml:"limit,omitempty"`
|
||||
ShowNotes bool `yaml:"show_notes,omitempty"`
|
||||
ShowCount bool `yaml:"show_count,omitempty"`
|
||||
// SocialProofConfig represents social proof section
|
||||
type SocialProofConfig struct {
|
||||
Logos []string `yaml:"logos,omitempty"`
|
||||
Testimonial TestimonialConfig `yaml:"testimonial,omitempty"`
|
||||
Testimonials []TestimonialConfig `yaml:"testimonials,omitempty"`
|
||||
}
|
||||
|
||||
// DocumentationConfig represents documentation template settings
|
||||
type DocumentationConfig struct {
|
||||
Source string `yaml:"source,omitempty"`
|
||||
Search bool `yaml:"search,omitempty"`
|
||||
TOC bool `yaml:"toc,omitempty"`
|
||||
Sidebar []SidebarGroup `yaml:"sidebar,omitempty"`
|
||||
EditLinks EditLinksConfig `yaml:"edit_links,omitempty"`
|
||||
// TestimonialConfig represents a testimonial
|
||||
type TestimonialConfig struct {
|
||||
Quote string `yaml:"quote,omitempty"`
|
||||
Author string `yaml:"author,omitempty"`
|
||||
Role string `yaml:"role,omitempty"`
|
||||
Avatar string `yaml:"avatar,omitempty"`
|
||||
}
|
||||
|
||||
// SidebarGroup represents a sidebar navigation group
|
||||
type SidebarGroup struct {
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Items []string `yaml:"items,omitempty"`
|
||||
Collapsed bool `yaml:"collapsed,omitempty"`
|
||||
// PricingConfig represents pricing section
|
||||
type PricingConfig struct {
|
||||
Headline string `yaml:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty"`
|
||||
Plans []PricingPlanConfig `yaml:"plans,omitempty"`
|
||||
}
|
||||
|
||||
// EditLinksConfig represents edit link settings
|
||||
type EditLinksConfig struct {
|
||||
Enabled bool `yaml:"enabled,omitempty"`
|
||||
Text string `yaml:"text,omitempty"`
|
||||
// PricingPlanConfig represents a pricing plan
|
||||
type PricingPlanConfig struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Price string `yaml:"price,omitempty"`
|
||||
Period string `yaml:"period,omitempty"`
|
||||
Features []string `yaml:"features,omitempty"`
|
||||
CTA string `yaml:"cta,omitempty"`
|
||||
Featured bool `yaml:"featured,omitempty"`
|
||||
}
|
||||
|
||||
// GalleryConfig represents gallery/portfolio template settings
|
||||
type GalleryConfig struct {
|
||||
Source string `yaml:"source,omitempty"`
|
||||
Columns int `yaml:"columns,omitempty"`
|
||||
Lightbox bool `yaml:"lightbox,omitempty"`
|
||||
Captions bool `yaml:"captions,omitempty"`
|
||||
Items []GalleryItem `yaml:"items,omitempty"`
|
||||
}
|
||||
|
||||
// GalleryItem represents a single gallery item
|
||||
type GalleryItem struct {
|
||||
Image string `yaml:"image,omitempty"`
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Link string `yaml:"link,omitempty"`
|
||||
// CTASectionConfig represents the final CTA section
|
||||
type CTASectionConfig struct {
|
||||
Headline string `yaml:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty"`
|
||||
Button CTAButton `yaml:"button,omitempty"`
|
||||
}
|
||||
|
||||
// FooterConfig represents footer settings
|
||||
type FooterConfig struct {
|
||||
Links []FooterLinkGroup `yaml:"links,omitempty"`
|
||||
Copyright string `yaml:"copyright,omitempty"`
|
||||
ShowPoweredBy bool `yaml:"show_powered_by,omitempty"`
|
||||
}
|
||||
|
||||
// FooterLinkGroup represents a group of footer links
|
||||
type FooterLinkGroup struct {
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Items []FooterLink `yaml:"items,omitempty"`
|
||||
Links []FooterLink `yaml:"links,omitempty"`
|
||||
Social []SocialLink `yaml:"social,omitempty"`
|
||||
Copyright string `yaml:"copyright,omitempty"`
|
||||
ShowPoweredBy bool `yaml:"show_powered_by,omitempty"`
|
||||
}
|
||||
|
||||
// FooterLink represents a single footer link
|
||||
type FooterLink struct {
|
||||
Text string `yaml:"text,omitempty"`
|
||||
URL string `yaml:"url,omitempty"`
|
||||
Label string `yaml:"label,omitempty"`
|
||||
URL string `yaml:"url,omitempty"`
|
||||
}
|
||||
|
||||
// SocialLink represents a social media link
|
||||
type SocialLink struct {
|
||||
Platform string `yaml:"platform,omitempty"` // twitter, github, discord, linkedin, youtube
|
||||
URL string `yaml:"url,omitempty"`
|
||||
}
|
||||
|
||||
// ThemeConfig represents theme customization
|
||||
type ThemeConfig struct {
|
||||
PrimaryColor string `yaml:"primary_color,omitempty"`
|
||||
AccentColor string `yaml:"accent_color,omitempty"`
|
||||
Mode string `yaml:"mode,omitempty"` // light, dark, auto
|
||||
}
|
||||
|
||||
// SEOConfig represents SEO and social sharing settings
|
||||
@@ -186,15 +200,14 @@ type AdvancedConfig struct {
|
||||
CustomCSS string `yaml:"custom_css,omitempty"`
|
||||
CustomHead string `yaml:"custom_head,omitempty"`
|
||||
Redirects map[string]string `yaml:"redirects,omitempty"`
|
||||
PublicReleases bool `yaml:"public_releases,omitempty"` // Allow public access to releases even for private repos
|
||||
PublicReleases bool `yaml:"public_releases,omitempty"`
|
||||
}
|
||||
|
||||
// ParseLandingConfig parses a landing.yaml file content
|
||||
func ParseLandingConfig(content []byte) (*LandingConfig, error) {
|
||||
config := &LandingConfig{
|
||||
// Set defaults
|
||||
Enabled: true,
|
||||
Template: "simple",
|
||||
Template: "open-source-hero",
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(content, config); err != nil {
|
||||
@@ -203,13 +216,10 @@ func ParseLandingConfig(content []byte) (*LandingConfig, error) {
|
||||
|
||||
// Apply defaults
|
||||
if config.Template == "" {
|
||||
config.Template = "simple"
|
||||
config.Template = "open-source-hero"
|
||||
}
|
||||
if config.Branding.DarkMode == "" {
|
||||
config.Branding.DarkMode = "auto"
|
||||
}
|
||||
if config.Gallery.Columns == 0 {
|
||||
config.Gallery.Columns = 4
|
||||
if config.Theme.Mode == "" {
|
||||
config.Theme.Mode = "auto"
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -225,36 +235,62 @@ func HashConfig(content []byte) string {
|
||||
func DefaultConfig() *LandingConfig {
|
||||
return &LandingConfig{
|
||||
Enabled: true,
|
||||
Template: "simple",
|
||||
Branding: BrandingConfig{
|
||||
DarkMode: "auto",
|
||||
},
|
||||
Template: "open-source-hero",
|
||||
Hero: HeroConfig{
|
||||
CTAPrimary: CTAConfig{
|
||||
Text: "Get Started",
|
||||
Link: "#installation",
|
||||
Headline: "Build something amazing",
|
||||
Subheadline: "A powerful toolkit for developers who want to ship fast.",
|
||||
PrimaryCTA: CTAButton{
|
||||
Label: "Get Started",
|
||||
URL: "#",
|
||||
},
|
||||
CTASecondary: CTAConfig{
|
||||
Text: "View Documentation",
|
||||
Link: "#readme",
|
||||
SecondaryCTA: CTAButton{
|
||||
Label: "View on GitHub",
|
||||
URL: "#",
|
||||
},
|
||||
},
|
||||
Sections: []SectionConfig{
|
||||
{Type: "readme"},
|
||||
{Type: "releases", Limit: 3},
|
||||
Stats: []StatConfig{
|
||||
{Value: "10k+", Label: "Downloads"},
|
||||
{Value: "100+", Label: "Contributors"},
|
||||
{Value: "MIT", Label: "License"},
|
||||
},
|
||||
ValueProps: []ValuePropConfig{
|
||||
{Title: "Fast", Description: "Optimized for performance out of the box.", Icon: "zap"},
|
||||
{Title: "Flexible", Description: "Adapts to your workflow, not the other way around.", Icon: "gear"},
|
||||
{Title: "Open Source", Description: "Free forever. Community driven.", Icon: "heart"},
|
||||
},
|
||||
CTASection: CTASectionConfig{
|
||||
Headline: "Ready to get started?",
|
||||
Subheadline: "Join thousands of developers already using this project.",
|
||||
Button: CTAButton{
|
||||
Label: "Get Started Free",
|
||||
URL: "#",
|
||||
},
|
||||
},
|
||||
Footer: FooterConfig{
|
||||
ShowPoweredBy: true,
|
||||
},
|
||||
Theme: ThemeConfig{
|
||||
Mode: "auto",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidTemplates returns the list of valid template names
|
||||
func ValidTemplates() []string {
|
||||
return []string{"simple", "documentation", "product", "portfolio"}
|
||||
return []string{"open-source-hero", "minimalist-docs", "saas-conversion", "bold-marketing"}
|
||||
}
|
||||
|
||||
// IsValidTemplate checks if a template name is valid
|
||||
func IsValidTemplate(name string) bool {
|
||||
return slices.Contains(ValidTemplates(), name)
|
||||
}
|
||||
|
||||
// TemplateDisplayNames returns a map of template names to display names
|
||||
func TemplateDisplayNames() map[string]string {
|
||||
return map[string]string{
|
||||
"open-source-hero": "Open Source Hero",
|
||||
"minimalist-docs": "Minimalist Docs",
|
||||
"saas-conversion": "SaaS Conversion",
|
||||
"bold-marketing": "Bold Marketing",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,9 +53,24 @@ type RepositoryStruct struct {
|
||||
GitGuideRemoteName *config.Value[string]
|
||||
}
|
||||
|
||||
type ThemeStruct struct {
|
||||
DisableRegistration *config.Value[bool]
|
||||
HideExploreUsers *config.Value[bool]
|
||||
HelpURL *config.Value[string]
|
||||
CustomSiteIconURL *config.Value[string]
|
||||
CustomHomeLogoURL *config.Value[string]
|
||||
CustomHomeHTML *config.Value[string]
|
||||
APIHeaderURL *config.Value[string]
|
||||
CustomHomeTitle *config.Value[string]
|
||||
CustomHomeTagline *config.Value[string]
|
||||
PinnedOrgDisplayFormat *config.Value[string]
|
||||
ExploreOrgDisplayFormat *config.Value[string]
|
||||
}
|
||||
|
||||
type ConfigStruct struct {
|
||||
Picture *PictureStruct
|
||||
Repository *RepositoryStruct
|
||||
Theme *ThemeStruct
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -74,6 +89,19 @@ func initDefaultConfig() {
|
||||
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
|
||||
GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"),
|
||||
},
|
||||
Theme: &ThemeStruct{
|
||||
DisableRegistration: config.ValueJSON[bool]("theme.disable_registration").WithFileConfig(config.CfgSecKey{Sec: "service", Key: "DISABLE_REGISTRATION"}),
|
||||
HideExploreUsers: config.ValueJSON[bool]("theme.hide_explore_users").WithDefault(false),
|
||||
HelpURL: config.ValueJSON[string]("theme.help_url").WithDefault(""),
|
||||
CustomSiteIconURL: config.ValueJSON[string]("theme.custom_site_icon_url").WithDefault(""),
|
||||
CustomHomeLogoURL: config.ValueJSON[string]("theme.custom_home_logo_url").WithDefault(""),
|
||||
CustomHomeHTML: config.ValueJSON[string]("theme.custom_home_html").WithDefault(""),
|
||||
APIHeaderURL: config.ValueJSON[string]("theme.api_header_url").WithDefault(""),
|
||||
CustomHomeTitle: config.ValueJSON[string]("theme.custom_home_title").WithDefault(""),
|
||||
CustomHomeTagline: config.ValueJSON[string]("theme.custom_home_tagline").WithDefault(""),
|
||||
PinnedOrgDisplayFormat: config.ValueJSON[string]("theme.pinned_org_display_format").WithDefault("condensed"),
|
||||
ExploreOrgDisplayFormat: config.ValueJSON[string]("theme.explore_org_display_format").WithDefault("list"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvConfigKeyPrefixGitCaddy is the primary prefix for GitCaddy environment variables
|
||||
EnvConfigKeyPrefixGitCaddy = "GITCADDY__"
|
||||
// EnvConfigKeyPrefixGitea is the legacy prefix for backward compatibility
|
||||
EnvConfigKeyPrefixGitea = "GITEA__"
|
||||
EnvConfigKeySuffixFile = "__FILE"
|
||||
)
|
||||
@@ -24,7 +27,8 @@ var escapeRegex = regexp.MustCompile(escapeRegexpString)
|
||||
|
||||
func CollectEnvConfigKeys() (keys []string) {
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, EnvConfigKeyPrefixGitea) {
|
||||
// Support both GITCADDY__ and GITEA__ prefixes
|
||||
if strings.HasPrefix(env, EnvConfigKeyPrefixGitCaddy) || strings.HasPrefix(env, EnvConfigKeyPrefixGitea) {
|
||||
k, _, _ := strings.Cut(env, "=")
|
||||
keys = append(keys, k)
|
||||
}
|
||||
@@ -41,7 +45,7 @@ func ClearEnvConfigKeys() {
|
||||
// decodeEnvSectionKey will decode a portable string encoded Section__Key pair
|
||||
// Portable strings are considered to be of the form [A-Z0-9_]*
|
||||
// We will encode a disallowed value as the UTF8 byte string preceded by _0X and
|
||||
// followed by _. E.g. _0X2C_ for a '-' and _0X2E_ for '.'
|
||||
// followed by _. E.g. _0X2C_ for a '-', and _0X2E_ for '.'.
|
||||
// Section and Key are separated by a plain '__'.
|
||||
// The entire section can be encoded as a UTF8 byte string
|
||||
func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
|
||||
@@ -96,16 +100,22 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
|
||||
}
|
||||
|
||||
// decodeEnvironmentKey decode the environment key to section and key
|
||||
// The environment key is in the form of GITEA__SECTION__KEY or GITEA__SECTION__KEY__FILE
|
||||
func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) {
|
||||
if !strings.HasPrefix(envKey, prefixGitea) {
|
||||
// The environment key is in the form of GITCADDY__SECTION__KEY or GITEA__SECTION__KEY (legacy)
|
||||
// or GITCADDY__SECTION__KEY__FILE / GITEA__SECTION__KEY__FILE
|
||||
func decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) {
|
||||
var prefix string
|
||||
if strings.HasPrefix(envKey, prefixGitCaddy) {
|
||||
prefix = prefixGitCaddy
|
||||
} else if strings.HasPrefix(envKey, prefixGitea) {
|
||||
prefix = prefixGitea
|
||||
} else {
|
||||
return false, "", "", false
|
||||
}
|
||||
if strings.HasSuffix(envKey, suffixFile) {
|
||||
useFileValue = true
|
||||
envKey = envKey[:len(envKey)-len(suffixFile)]
|
||||
}
|
||||
ok, section, key = decodeEnvSectionKey(envKey[len(prefixGitea):])
|
||||
ok, section, key = decodeEnvSectionKey(envKey[len(prefix):])
|
||||
return ok, section, key, useFileValue
|
||||
}
|
||||
|
||||
@@ -119,7 +129,7 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
|
||||
// parse the environment variable to config section name and key name
|
||||
envKey := before
|
||||
envValue := after
|
||||
ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(EnvConfigKeyPrefixGitea, EnvConfigKeySuffixFile, envKey)
|
||||
ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(EnvConfigKeyPrefixGitCaddy, EnvConfigKeyPrefixGitea, EnvConfigKeySuffixFile, envKey)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@@ -167,20 +177,25 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
|
||||
return changed
|
||||
}
|
||||
|
||||
// InitGiteaEnvVars initializes the environment variables for gitea
|
||||
func InitGiteaEnvVars() {
|
||||
// Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want,
|
||||
// InitGitCaddyEnvVars initializes the environment variables for GitCaddy Server
|
||||
func InitGitCaddyEnvVars() {
|
||||
// Ideally GitCaddy should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want,
|
||||
// but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users,
|
||||
// so at the moment we could still keep "unsetting the unnecessary environments"
|
||||
|
||||
// HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig".
|
||||
// HOME is managed by GitCaddy, GitCaddy's git should use "HOME/.gitconfig".
|
||||
// But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist,
|
||||
// then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set.
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
|
||||
// InitGiteaEnvVars is an alias for InitGitCaddyEnvVars for backward compatibility
|
||||
func InitGiteaEnvVars() {
|
||||
InitGitCaddyEnvVars()
|
||||
}
|
||||
|
||||
func InitGiteaEnvVarsForTesting() {
|
||||
InitGiteaEnvVars()
|
||||
InitGitCaddyEnvVars()
|
||||
_ = os.Unsetenv("GIT_AUTHOR_NAME")
|
||||
_ = os.Unsetenv("GIT_AUTHOR_EMAIL")
|
||||
_ = os.Unsetenv("GIT_AUTHOR_DATE")
|
||||
|
||||
@@ -33,28 +33,29 @@ func TestDecodeEnvSectionKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDecodeEnvironmentKey(t *testing.T) {
|
||||
prefix := "GITEA__"
|
||||
prefixGitCaddy := "GITCADDY__"
|
||||
prefixGitea := "GITEA__"
|
||||
suffix := "__FILE"
|
||||
|
||||
ok, section, key, file := decodeEnvironmentKey(prefix, suffix, "SEC__KEY")
|
||||
ok, section, key, file := decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "SEC__KEY")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Empty(t, key)
|
||||
assert.False(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC")
|
||||
ok, section, key, file = decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "GITEA__SEC")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Empty(t, key)
|
||||
assert.False(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA____KEY")
|
||||
ok, section, key, file = decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "GITEA____KEY")
|
||||
assert.True(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Equal(t, "KEY", key)
|
||||
assert.False(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC__KEY")
|
||||
ok, section, key, file = decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "GITEA__SEC__KEY")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "sec", section)
|
||||
assert.Equal(t, "KEY", key)
|
||||
@@ -62,19 +63,19 @@ func TestDecodeEnvironmentKey(t *testing.T) {
|
||||
|
||||
// with "__FILE" suffix, it doesn't support to write "[sec].FILE" to config (no such key FILE is used in Gitea)
|
||||
// but it could be fixed in the future by adding a new suffix like "__VALUE" (no such key VALUE is used in Gitea either)
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC__FILE")
|
||||
ok, section, key, file = decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "GITEA__SEC__FILE")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Empty(t, key)
|
||||
assert.True(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC__KEY__FILE")
|
||||
ok, section, key, file = decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "GITEA__SEC__KEY__FILE")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "sec", section)
|
||||
assert.Equal(t, "KEY", key)
|
||||
assert.True(t, file)
|
||||
|
||||
ok, _, _, _ = decodeEnvironmentKey("PREFIX__", "", "PREFIX__SEC__KEY")
|
||||
ok, _, _, _ = decodeEnvironmentKey("PREFIX__", "", "", "PREFIX__SEC__KEY")
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ var (
|
||||
// AppPath represents the path to the gitea binary
|
||||
AppPath string
|
||||
|
||||
// AppWorkPath is the "working directory" of Gitea. It maps to the: WORK_PATH in app.ini, "--work-path" flag, environment variable GITEA_WORK_DIR.
|
||||
// AppWorkPath is the "working directory" of GitCaddy. It maps to the: WORK_PATH in app.ini, "--work-path" flag, environment variable GITCADDY_WORK_DIR (or GITEA_WORK_DIR for backward compatibility).
|
||||
// If that is not set it is the default set here by the linker or failing that the directory of AppPath.
|
||||
// It is used as the base path for several other paths.
|
||||
AppWorkPath string
|
||||
CustomPath string // Custom directory path. Env: GITEA_CUSTOM
|
||||
CustomPath string // Custom directory path. Env: GITCADDY_CUSTOM (or GITEA_CUSTOM for backward compatibility)
|
||||
CustomConf string
|
||||
|
||||
appWorkPathBuiltin string
|
||||
|
||||
@@ -183,7 +183,7 @@ func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string {
|
||||
|
||||
func loadServerFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("server")
|
||||
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
|
||||
AppName = rootCfg.Section("").Key("APP_NAME").MustString("GitCaddy")
|
||||
|
||||
Domain = sec.Key("DOMAIN").MustString("localhost")
|
||||
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
|
||||
|
||||
@@ -96,7 +96,7 @@ func InitCfgProvider(file string) {
|
||||
|
||||
func MustInstalled() {
|
||||
if !InstallLock {
|
||||
log.Fatal(`Unable to load config file for a installed Gitea instance, you should either use "--config" to set your config file (app.ini), or run "gitea web" command to install Gitea.`)
|
||||
log.Fatal(`Unable to load config file for a installed GitCaddy instance, you should either use "--config" to set your config file (app.ini), or run "gitcaddy-server web" command to install GitCaddy.`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,9 +176,9 @@ func loadRunModeFrom(rootCfg ConfigProvider) {
|
||||
if os.Getuid() == 0 {
|
||||
if !unsafeAllowRunAsRoot {
|
||||
// Special thanks to VLC which inspired the wording of this messaging.
|
||||
log.Fatal("Gitea is not supposed to be run as root. Sorry. If you need to use privileged TCP ports please instead use setcap and the `cap_net_bind_service` permission")
|
||||
log.Fatal("GitCaddy Server is not supposed to be run as root. Sorry. If you need to use privileged TCP ports please instead use setcap and the `cap_net_bind_service` permission")
|
||||
}
|
||||
log.Critical("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
|
||||
log.Critical("You are running GitCaddy Server using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,16 +28,27 @@ type DistroInfo struct {
|
||||
PrettyName string `json:"pretty_name,omitempty"` // e.g., "Ubuntu 24.04 LTS"
|
||||
}
|
||||
|
||||
// XcodeInfo holds Xcode-specific information for macOS runners
|
||||
type XcodeInfo struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
Build string `json:"build,omitempty"`
|
||||
SDKs []string `json:"sdks,omitempty"`
|
||||
Simulators []string `json:"simulators,omitempty"`
|
||||
}
|
||||
|
||||
// RunnerCapability represents the detailed capabilities of a runner
|
||||
type RunnerCapability struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Distro *DistroInfo `json:"distro,omitempty"`
|
||||
Xcode *XcodeInfo `json:"xcode,omitempty"`
|
||||
Docker bool `json:"docker"`
|
||||
DockerCompose bool `json:"docker_compose"`
|
||||
ContainerRuntime string `json:"container_runtime,omitempty"`
|
||||
Shell []string `json:"shell,omitempty"`
|
||||
Tools map[string][]string `json:"tools,omitempty"`
|
||||
BuildTools []string `json:"build_tools,omitempty"`
|
||||
PackageManagers []string `json:"package_managers,omitempty"`
|
||||
Features *CapabilityFeatures `json:"features,omitempty"`
|
||||
Limitations []string `json:"limitations,omitempty"`
|
||||
Disk *DiskInfo `json:"disk,omitempty"`
|
||||
|
||||
@@ -39,6 +39,7 @@ func NewFuncMap() template.FuncMap {
|
||||
"QueryEscape": queryEscape,
|
||||
"QueryBuild": QueryBuild,
|
||||
"SanitizeHTML": SanitizeHTML,
|
||||
"SafeHTML": SafeHTML,
|
||||
"URLJoin": util.URLJoin,
|
||||
"DotEscape": dotEscape,
|
||||
|
||||
@@ -172,6 +173,11 @@ func SanitizeHTML(s string) template.HTML {
|
||||
return markup.Sanitize(s)
|
||||
}
|
||||
|
||||
// SafeHTML marks a string as safe HTML (no sanitization). Use with caution - only for trusted admin content.
|
||||
func SafeHTML(s string) template.HTML {
|
||||
return template.HTML(s)
|
||||
}
|
||||
|
||||
func htmlFormat(s any, args ...any) template.HTML {
|
||||
if len(args) == 0 {
|
||||
// to prevent developers from calling "HTMLFormat $userInput" by mistake which will lead to XSS
|
||||
|
||||
@@ -174,14 +174,14 @@
|
||||
"error.report_message": "Pokud jste si jisti, že se jedná o chybu Gitea, prosím vyhledejte problémy na <a href=\"%s\" target=\"_blank\">GitHub</a> a v případě potřeby založte nový problém.",
|
||||
"error.not_found": "Cíl nebyl nalezen.",
|
||||
"error.network_error": "Chyba sítě",
|
||||
"startpage.app_desc": "Snadno přístupný vlastní Git",
|
||||
"startpage.install": "Jednoduchá na instalaci",
|
||||
"startpage.install_desc": "Jednoduše <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">spusťte jako binární program</a> pro vaši platformu, nasaďte jej pomocí <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Docker</a>, nebo jej stáhněte jako <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">balíček</a>.",
|
||||
"startpage.platform": "Multiplatformní",
|
||||
"startpage.platform_desc": "Gitea běží všude, kde <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Go</a> může kompilovat: Windows, macOS, Linux, ARM, atd. Vyberte si ten, který milujete!",
|
||||
"startpage.lightweight": "Lehká",
|
||||
"startpage.lightweight_desc": "Gitea má minimální požadavky a může běžet na Raspberry Pi. Šetřete energii vašeho stroje!",
|
||||
"startpage.license_desc": "Vše je na <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[1]s\">%[2]s</a>! Připojte se tím, že <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">přispějete</a> a uděláte tento projekt ještě lepší. Nestyďte se být přispěvatel!",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.install_desc": "Run the binary, deploy with Docker, or use your favorite package manager. GitCaddy runs wherever you need it.",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.platform_desc": "Built for the age of AI-assisted development. Structured APIs, capability discovery, and intelligent context for AI tools.",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license_desc": "GitCaddy is open source and built on the shoulders of giants. Based on Gitea, enhanced for the AI era.",
|
||||
"install.install": "Instalace",
|
||||
"install.title": "Výchozí konfigurace",
|
||||
"install.docker_helper": "Pokud spouštíte Gitea v Dockeru, přečtěte si <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">dokumentaci</a>, než budete měnit jakákoliv nastavení.",
|
||||
|
||||
@@ -149,12 +149,12 @@
|
||||
"error.occurred": "Παρουσιάστηκε ένα σφάλμα",
|
||||
"error.not_found": "Ο προορισμός δεν βρέθηκε.",
|
||||
"error.network_error": "Σφάλμα δικτύου",
|
||||
"startpage.app_desc": "Μια ανώδυνη, αυτο-φιλοξενούμενη υπηρεσία Git",
|
||||
"startpage.install": "Εύκολο στην εγκατάσταση",
|
||||
"startpage.platform": "Πολυπλατφορμικό",
|
||||
"startpage.lightweight": "Ελαφρύ",
|
||||
"startpage.lightweight_desc": "Gitea έχει χαμηλές ελάχιστες απαιτήσεις και μπορεί να τρέξει σε ένα οικονομικό Raspberry Pi. Εξοικονομήστε ενέργεια!",
|
||||
"startpage.license": "Ανοικτού κώδικα",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license": "Open Source",
|
||||
"install.install": "Εγκατάσταση",
|
||||
"install.title": "Αρχικές Ρυθμίσεις",
|
||||
"install.docker_helper": "Αν εκτελέσετε το Gitea μέσα στο Docker, παρακαλώ διαβάστε την <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">τεκμηρίωση</a> πριν αλλάξετε τις ρυθμίσεις.",
|
||||
|
||||
@@ -87,12 +87,12 @@
|
||||
"filter.public": "عمومی",
|
||||
"filter.private": "خصوصی",
|
||||
"editor.buttons.table.add.insert": "افزودن",
|
||||
"startpage.app_desc": "یک سرویس گیت بیدرد سر و راحت",
|
||||
"startpage.install": "راهاندازی ساده",
|
||||
"startpage.platform": "مستقل از سکو",
|
||||
"startpage.lightweight": "ابزارک سبک",
|
||||
"startpage.lightweight_desc": "گیتی با حداقل منابع میتوانید برای روی دستگاه Raspberry Pi اجرا شود و مصرف انرژی شما را کاهش دهد!",
|
||||
"startpage.license": "متن باز",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license": "Open Source",
|
||||
"install.install": "نصب و راه اندازی",
|
||||
"install.title": "تنظیمات اولیه",
|
||||
"install.docker_helper": "اگر گیتی را با داکر اجرا کردهاید، لطفا قبل از هر تغییری <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">مستندات</a> را مطالعه نمایید.",
|
||||
|
||||
@@ -101,12 +101,12 @@
|
||||
"error.occurred": "Virhe tapahtui",
|
||||
"error.not_found": "Kohdetta ei löytynyt.",
|
||||
"error.network_error": "Verkkovirhe",
|
||||
"startpage.app_desc": "Kivuton, itsehostattu Git-palvelu",
|
||||
"startpage.install": "Helppo asentaa",
|
||||
"startpage.platform": "Alustariippumaton",
|
||||
"startpage.lightweight": "Kevyt",
|
||||
"startpage.lightweight_desc": "Gitealla on vähäiset vähimmäisvaatimukset, joten se toimii jopa halvassa Raspberry Pi:ssä. Säästä koneesi energiaa!",
|
||||
"startpage.license": "Avoin lähdekoodi",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license": "Open Source",
|
||||
"install.install": "Asennus",
|
||||
"install.title": "Alkuperäiset asetukset",
|
||||
"install.docker_helper": "Jos ajat Giteaa Dockerin sisällä, lue <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">ohjeet</a> ennen minkään asetuksen muuttamista.",
|
||||
|
||||
@@ -207,15 +207,15 @@
|
||||
"error.report_message": "Má chreideann tú gur fabht Gitea é seo, déan cuardach le haghaidh ceisteanna ar <a href=\"%s\" target=\"_blank\">GitHub</a> nó oscail eagrán nua más gá.",
|
||||
"error.not_found": "Ní raibh an sprioc in ann a fháil.",
|
||||
"error.network_error": "Earráid líonra",
|
||||
"startpage.app_desc": "Seirbhís Git gan phian, féin-óstáil",
|
||||
"startpage.install": "Éasca a shuiteáil",
|
||||
"startpage.install_desc": "Níl ort ach <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[1]s\">rith an dénártha</a> do d'ardán, seol é le <a target=\"_blank\" rel=\"noopener noreferrer \" href=\"%[2]s\">Docker</a>, nó faigh <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">pacáilte</a> é.",
|
||||
"startpage.platform": "Tras-ardán",
|
||||
"startpage.platform_desc": "Ritheann Gitea áit ar <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://go.dev/\">bith is féidir le Go</a> tiomsú le haghaidh: Windows, macOS, Linux, ARM, srl Roghnaigh an ceann is breá leat!",
|
||||
"startpage.lightweight": "Éadrom",
|
||||
"startpage.lightweight_desc": "Tá íosta riachtanais íseal ag Gitea agus is féidir leo rith ar Raspberry Pi saor. Sábháil fuinneamh do mheaisín!",
|
||||
"startpage.license": "Foinse Oscailte",
|
||||
"startpage.license_desc": "Téigh go bhfaighidh <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[1]s\">%[2]s</a>! Bí linn trí <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">cur leis</a> chun an tionscadal seo a fheabhsú fós. Ná bíodh cúthail ort a bheith i do rannpháirtí!",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.install_desc": "Run the binary, deploy with Docker, or use your favorite package manager. GitCaddy runs wherever you need it.",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.platform_desc": "Built for the age of AI-assisted development. Structured APIs, capability discovery, and intelligent context for AI tools.",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license": "Open Source",
|
||||
"startpage.license_desc": "GitCaddy is open source and built on the shoulders of giants. Based on Gitea, enhanced for the AI era.",
|
||||
"install.install": "Suiteáil",
|
||||
"install.installing_desc": "Suiteáil anois, fan go fóill…",
|
||||
"install.title": "Cumraíocht Tosaigh",
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
"filter.public": "Nyilvános",
|
||||
"filter.private": "Privát",
|
||||
"editor.buttons.table.add.insert": "Hozzáadás",
|
||||
"startpage.app_desc": "Fájdalommentes, saját gépre telepíthető Git szolgáltatás",
|
||||
"startpage.install": "Könnyen telepíthető",
|
||||
"startpage.platform": "Keresztplatformos",
|
||||
"startpage.lightweight": "Könnyűsúlyú",
|
||||
"startpage.license": "Nyílt forráskódú",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.license": "Open Source",
|
||||
"install.install": "Telepítés",
|
||||
"install.title": "Kezdeti konfiguráció",
|
||||
"install.docker_helper": "Ha ön a Gitea-t Docker-ből futtatja, kérem olvassa el a <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">dokumentációt</a> a beállítások megváltoztatása előtt.",
|
||||
|
||||
@@ -147,12 +147,12 @@
|
||||
"error.occurred": "Terjadi kesalahan",
|
||||
"error.report_message": "Jika Anda yakin ini adalah bug Gitea, silakan cari isu di <a href=\"%s\" target=\"_blank\">GitHub</a> atau buka isu baru jika diperlukan.",
|
||||
"error.not_found": "Target tidak dapat ditemukan.",
|
||||
"startpage.app_desc": "Sebuah layanan hosting Git sendiri yang tanpa kesulitan",
|
||||
"startpage.install": "Mudah dipasang",
|
||||
"startpage.platform": "Lintas platform",
|
||||
"startpage.lightweight": "Ringan",
|
||||
"startpage.lightweight_desc": "Gitea hanya membutuhkan persyaratan minimal dan bisa berjalan pada Raspberry Pi yang murah. Bisa menghemat listrik!",
|
||||
"startpage.license": "Sumber Terbuka",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license": "Open Source",
|
||||
"install.title": "Konfigurasi Awal",
|
||||
"install.user": "Nama Pengguna",
|
||||
"install.password": "Kata Sandi",
|
||||
|
||||
@@ -100,12 +100,12 @@
|
||||
"error.occurred": "Villa kom upp",
|
||||
"error.not_found": "Markmiðið fannst ekki.",
|
||||
"error.network_error": "Netkerfisvilla",
|
||||
"startpage.app_desc": "Þrautalaus og sjálfhýst Git þjónusta",
|
||||
"startpage.install": "Einföld uppsetning",
|
||||
"startpage.platform": "Fjölvettvangur",
|
||||
"startpage.lightweight": "Létt",
|
||||
"startpage.lightweight_desc": "Gitea hefur lágar lágmarkskröfur og getur keyrt á ódýrum Raspberry Pi. Sparaðu orku!",
|
||||
"startpage.license": "Frjáls Hugbúnaður",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license": "Open Source",
|
||||
"install.install": "Uppsetning",
|
||||
"install.title": "Upphafleg Uppsetning",
|
||||
"install.docker_helper": "Ef þú keyrir Gitea inni í Docker þá viltu vinsamlegast lesa <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">leiðbeiningaritið</a> áður en þú breytir stillingum.",
|
||||
|
||||
@@ -155,12 +155,12 @@
|
||||
"error.occurred": "Radusies kļūda",
|
||||
"error.not_found": "Pieprasītie dati netika atrasti.",
|
||||
"error.network_error": "Tīkla kļūda",
|
||||
"startpage.app_desc": "Viegli uzstādāms Git serviss",
|
||||
"startpage.install": "Vienkārši instalējams",
|
||||
"startpage.platform": "Pieejama dažādām platformām",
|
||||
"startpage.lightweight": "Viegla",
|
||||
"startpage.lightweight_desc": "Gitea ir miminālas prasības un to var darbināt uz nedārga Raspberry Pi datora. Ietaupi savai ierīcei resursus!",
|
||||
"startpage.license": "Atvērtā pirmkoda",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license": "Open Source",
|
||||
"install.install": "Instalācija",
|
||||
"install.title": "Sākotnējā konfigurācija",
|
||||
"install.docker_helper": "Ja Gitea ir uzstādīts Docker konteinerī, izlasiet <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">vadlīninas</a> pirms maināt iestatījumus.",
|
||||
|
||||
@@ -87,12 +87,12 @@
|
||||
"filter.public": "ප්රසිද්ධ",
|
||||
"filter.private": "පෞද්ගලික",
|
||||
"editor.buttons.table.add.insert": "එකතු",
|
||||
"startpage.app_desc": "වේදනාකාරී, ස්වයං-සත්කාරක Git සේවාවක්",
|
||||
"startpage.install": "ස්ථාපනයට පහසුය",
|
||||
"startpage.platform": "හරස් වේදිකාව",
|
||||
"startpage.lightweight": "සැහැල්ලු",
|
||||
"startpage.lightweight_desc": "Gitea අඩු අවම අවශ්යතා ඇති අතර මිල අඩු Raspberry Pi මත ධාවනය කළ හැකිය. ඔබේ යන්ත්ර ශක්තිය සුරකින්න!",
|
||||
"startpage.license": "විවෘත මූලාශ්ර",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license": "Open Source",
|
||||
"install.install": "ස්ථාපනය",
|
||||
"install.title": "මූලික වින්යාසය",
|
||||
"install.docker_helper": "ඔබ Docker තුළ Gitea ධාවනය කරන්නේ නම්, කරුණාකර ඕනෑම සැකසුම් වෙනස් කිරීමට පෙර <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">ලියකියවිලි</a> කියවන්න.",
|
||||
|
||||
@@ -147,12 +147,12 @@
|
||||
"error.occurred": "Vyskytla sa chyba",
|
||||
"error.not_found": "Nebolo možné nájsť cieľ.",
|
||||
"error.network_error": "Chyba siete",
|
||||
"startpage.app_desc": "Jednoducho prístupný vlastný Git",
|
||||
"startpage.install": "Jednoduchá inštalácia",
|
||||
"startpage.platform": "Multiplatformový",
|
||||
"startpage.lightweight": "Ľahká",
|
||||
"startpage.lightweight_desc": "Gitea má minimálne požiadavky a môže bežať na Raspberry Pi. Šetrite energiou vášho stroja!",
|
||||
"startpage.license": "Otvorený zdrojový kód",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license": "Open Source",
|
||||
"install.install": "Inštalácia",
|
||||
"install.title": "Východzia konfigurácia",
|
||||
"install.docker_helper": "Ak spúšťate Gitea v Docker kontajneri, prečítajte si <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">dokumentáciu</a> pred zmenou akýchkoľvek nastavení.",
|
||||
|
||||
@@ -74,12 +74,12 @@
|
||||
"filter.public": "Offentlig",
|
||||
"filter.private": "Privat",
|
||||
"editor.buttons.table.add.insert": "Lägg till",
|
||||
"startpage.app_desc": "En smidig, självhostad Git-tjänst",
|
||||
"startpage.install": "Lätt att installera",
|
||||
"startpage.platform": "Plattformsoberoende",
|
||||
"startpage.lightweight": "Lättviktig",
|
||||
"startpage.lightweight_desc": "Gitea har låga minimum-krav och kan köras på en billig Rasperry Pi. Spara på din maskins kraft!",
|
||||
"startpage.license": "Öppen källkod",
|
||||
"startpage.app_desc": "Steeped in your workflow",
|
||||
"startpage.install": "Deploy Anywhere",
|
||||
"startpage.platform": "AI-Native Platform",
|
||||
"startpage.lightweight": "Lightning Fast",
|
||||
"startpage.lightweight_desc": "Minimal footprint, maximum performance. GitCaddy runs efficiently on everything from Raspberry Pi to enterprise servers.",
|
||||
"startpage.license": "Open Source",
|
||||
"install.title": "Ursprunglig konfiguration",
|
||||
"install.docker_helper": "Om du kör Gitea i Docker, vänligen läs igenom <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">dokumentationen</a> innan några inställningar ändras.",
|
||||
"install.db_title": "Databasinställningar",
|
||||
|
||||
@@ -1 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="374px" height="374px" viewBox="397.5 371.5 374 374" style="enable-background:new 397.5 371.5 374 374;"
|
||||
xml:space="preserve">
|
||||
<path style="fill:#609926;" d="M526.05,605.17c-0.69,0-1.39-0.14-2.08-0.46l-82.52-37.42v95.65l138.07,48.38V563.78l-50.37,40.29
|
||||
C528.27,604.76,527.16,605.17,526.05,605.17L526.05,605.17z"/>
|
||||
<path style="fill:#609926;" d="M643.03,605.17c-1.16,0-2.26-0.42-3.15-1.11l-50.37-40.29v147.55l138.16-48.38v-95.65l-82.56,37.42
|
||||
c-0.65,0.28-1.34,0.46-2.04,0.46L643.03,605.17z"/>
|
||||
<path style="fill:#609926;" d="M525.31,594.35l49.08-39.22l-137-48.06l-37.88,30.34L525.31,594.35z"/>
|
||||
<path style="fill:#609926;" d="M594.69,555.13l49.03,39.22l125.81-56.94l-37.88-30.34L594.69,555.13z"/>
|
||||
<path style="fill:#609926;" d="M525.36,438.98c-11.01-3.42-23.22-7.26-36.22-13.46c-1.62,64.89,35.94,80.94,52.68,84.88
|
||||
c16.47,3.88,30.57,0.55,35.48-3.88c-11.24-14.06-25.81-26.6-43.43-31.45c16.19,1.76,31.13,9.62,43.99,19.89
|
||||
c-3.88-40.66-22.76-46.62-52.45-55.97H525.36z"/>
|
||||
<path style="fill:#609926;" d="M589.74,491.11c5.23,5,20.81,8.79,38.85,4.53c18.22-4.3,59.16-21.79,57.22-92.6
|
||||
c-14.2,6.8-27.57,10.96-39.59,14.76c-32.47,10.17-53.01,16.6-56.98,61.52c13.92-11.38,30.16-20.26,47.96-22.29
|
||||
c-19.24,5.23-35.2,18.87-47.41,34.04L589.74,491.11z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
19
public/assets/img/gitcaddy-icon.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="374px" height="374px" viewBox="397.5 371.5 374 374" style="enable-background:new 397.5 371.5 374 374;"
|
||||
xml:space="preserve">
|
||||
<path style="fill:#609926;" d="M526.05,605.17c-0.69,0-1.39-0.14-2.08-0.46l-82.52-37.42v95.65l138.07,48.38V563.78l-50.37,40.29
|
||||
C528.27,604.76,527.16,605.17,526.05,605.17L526.05,605.17z"/>
|
||||
<path style="fill:#609926;" d="M643.03,605.17c-1.16,0-2.26-0.42-3.15-1.11l-50.37-40.29v147.55l138.16-48.38v-95.65l-82.56,37.42
|
||||
c-0.65,0.28-1.34,0.46-2.04,0.46L643.03,605.17z"/>
|
||||
<path style="fill:#609926;" d="M525.31,594.35l49.08-39.22l-137-48.06l-37.88,30.34L525.31,594.35z"/>
|
||||
<path style="fill:#609926;" d="M594.69,555.13l49.03,39.22l125.81-56.94l-37.88-30.34L594.69,555.13z"/>
|
||||
<path style="fill:#609926;" d="M525.36,438.98c-11.01-3.42-23.22-7.26-36.22-13.46c-1.62,64.89,35.94,80.94,52.68,84.88
|
||||
c16.47,3.88,30.57,0.55,35.48-3.88c-11.24-14.06-25.81-26.6-43.43-31.45c16.19,1.76,31.13,9.62,43.99,19.89
|
||||
c-3.88-40.66-22.76-46.62-52.45-55.97H525.36z"/>
|
||||
<path style="fill:#609926;" d="M589.74,491.11c5.23,5,20.81,8.79,38.85,4.53c18.22-4.3,59.16-21.79,57.22-92.6
|
||||
c-14.2,6.8-27.57,10.96-39.59,14.76c-32.47,10.17-53.01,16.6-56.98,61.52c13.92-11.38,30.16-20.26,47.96-22.29
|
||||
c-19.24,5.23-35.2,18.87-47.41,34.04L589.74,491.11z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1062
public/assets/img/gitcaddy-logo.svg
Normal file
|
After Width: | Height: | Size: 105 KiB |
@@ -1 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="32" height="32"><path d="M395.9 484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z" fill="#fff"/><g fill="#609926"><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z"/></g></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="374px" height="374px" viewBox="397.5 371.5 374 374" style="enable-background:new 397.5 371.5 374 374;"
|
||||
xml:space="preserve">
|
||||
<path style="fill:#609926;" d="M526.05,605.17c-0.69,0-1.39-0.14-2.08-0.46l-82.52-37.42v95.65l138.07,48.38V563.78l-50.37,40.29
|
||||
C528.27,604.76,527.16,605.17,526.05,605.17L526.05,605.17z"/>
|
||||
<path style="fill:#609926;" d="M643.03,605.17c-1.16,0-2.26-0.42-3.15-1.11l-50.37-40.29v147.55l138.16-48.38v-95.65l-82.56,37.42
|
||||
c-0.65,0.28-1.34,0.46-2.04,0.46L643.03,605.17z"/>
|
||||
<path style="fill:#609926;" d="M525.31,594.35l49.08-39.22l-137-48.06l-37.88,30.34L525.31,594.35z"/>
|
||||
<path style="fill:#609926;" d="M594.69,555.13l49.03,39.22l125.81-56.94l-37.88-30.34L594.69,555.13z"/>
|
||||
<path style="fill:#609926;" d="M525.36,438.98c-11.01-3.42-23.22-7.26-36.22-13.46c-1.62,64.89,35.94,80.94,52.68,84.88
|
||||
c16.47,3.88,30.57,0.55,35.48-3.88c-11.24-14.06-25.81-26.6-43.43-31.45c16.19,1.76,31.13,9.62,43.99,19.89
|
||||
c-3.88-40.66-22.76-46.62-52.45-55.97H525.36z"/>
|
||||
<path style="fill:#609926;" d="M589.74,491.11c5.23,5,20.81,8.79,38.85,4.53c18.22-4.3,59.16-21.79,57.22-92.6
|
||||
c-14.2,6.8-27.57,10.96-39.59,14.76c-32.47,10.17-53.01,16.6-56.98,61.52c13.92-11.38,30.16-20.26,47.96-22.29
|
||||
c-19.24,5.23-35.2,18.87-47.41,34.04L589.74,491.11z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="374px" height="374px" viewBox="397.5 371.5 374 374" style="enable-background:new 397.5 371.5 374 374;"
|
||||
xml:space="preserve">
|
||||
<path style="fill:#609926;" d="M526.05,605.17c-0.69,0-1.39-0.14-2.08-0.46l-82.52-37.42v95.65l138.07,48.38V563.78l-50.37,40.29
|
||||
C528.27,604.76,527.16,605.17,526.05,605.17L526.05,605.17z"/>
|
||||
<path style="fill:#609926;" d="M643.03,605.17c-1.16,0-2.26-0.42-3.15-1.11l-50.37-40.29v147.55l138.16-48.38v-95.65l-82.56,37.42
|
||||
c-0.65,0.28-1.34,0.46-2.04,0.46L643.03,605.17z"/>
|
||||
<path style="fill:#609926;" d="M525.31,594.35l49.08-39.22l-137-48.06l-37.88,30.34L525.31,594.35z"/>
|
||||
<path style="fill:#609926;" d="M594.69,555.13l49.03,39.22l125.81-56.94l-37.88-30.34L594.69,555.13z"/>
|
||||
<path style="fill:#609926;" d="M525.36,438.98c-11.01-3.42-23.22-7.26-36.22-13.46c-1.62,64.89,35.94,80.94,52.68,84.88
|
||||
c16.47,3.88,30.57,0.55,35.48-3.88c-11.24-14.06-25.81-26.6-43.43-31.45c16.19,1.76,31.13,9.62,43.99,19.89
|
||||
c-3.88-40.66-22.76-46.62-52.45-55.97H525.36z"/>
|
||||
<path style="fill:#609926;" d="M589.74,491.11c5.23,5,20.81,8.79,38.85,4.53c18.22-4.3,59.16-21.79,57.22-92.6
|
||||
c-14.2,6.8-27.57,10.96-39.59,14.76c-32.47,10.17-53.01,16.6-56.98,61.52c13.92-11.38,30.16-20.26,47.96-22.29
|
||||
c-19.24,5.23-35.2,18.87-47.41,34.04L589.74,491.11z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
27639
public/assets/licenses.txt
Normal file
@@ -15,14 +15,13 @@ import (
|
||||
const scalarTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.AppName}} API v2 Reference</title>
|
||||
<title>GitCaddy API v2 Reference</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
/* Custom theme to match Gitea */
|
||||
:root {
|
||||
--scalar-color-1: #4a90d9;
|
||||
--scalar-color-accent: #4a90d9;
|
||||
--scalar-color-1: #609926;
|
||||
--scalar-color-accent: #609926;
|
||||
--scalar-background-1: #ffffff;
|
||||
--scalar-background-2: #f8f9fa;
|
||||
--scalar-background-3: #e9ecef;
|
||||
@@ -34,9 +33,42 @@ const scalarTemplate = `<!DOCTYPE html>
|
||||
--scalar-background-3: #333333;
|
||||
}
|
||||
}
|
||||
.api-version-bar {
|
||||
background: var(--scalar-background-2, #f8f9fa);
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--scalar-background-3, #e9ecef);
|
||||
}
|
||||
.api-version-bar a {
|
||||
color: #609926;
|
||||
text-decoration: none;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.api-version-bar a:hover {
|
||||
background: rgba(96, 153, 38, 0.1);
|
||||
}
|
||||
.api-version-bar a.active {
|
||||
background: #609926;
|
||||
color: white;
|
||||
}
|
||||
.api-version-bar .back-link {
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="api-version-bar">
|
||||
<a href="{{.AppSubURL}}/" class="back-link">← Back to GitCaddy</a>
|
||||
<span>API Version:</span>
|
||||
<a href="{{.AppSubURL}}/api/swagger">v1</a>
|
||||
<a href="{{.AppSubURL}}/api/v2/docs" class="active">v2</a>
|
||||
</div>
|
||||
<script
|
||||
id="api-reference"
|
||||
data-url="{{.SpecURL}}"
|
||||
@@ -60,8 +92,9 @@ const scalarTemplate = `<!DOCTYPE html>
|
||||
// DocsScalar serves the Scalar API documentation UI for v2
|
||||
func DocsScalar(ctx *context.APIContext) {
|
||||
data := map[string]string{
|
||||
"AppName": setting.AppName,
|
||||
"SpecURL": setting.AppSubURL + "/api/v2/swagger.json",
|
||||
"AppName": setting.AppName,
|
||||
"AppSubURL": setting.AppSubURL,
|
||||
"SpecURL": setting.AppSubURL + "/api/v2/swagger.json",
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
@@ -22,16 +22,13 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// RawMessage is a raw encoded JSON value (equivalent to encoding/json.RawMessage)
|
||||
type RawMessage []byte
|
||||
|
||||
// MCP Protocol Types (JSON-RPC 2.0)
|
||||
|
||||
type MCPRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID any `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params RawMessage `json:"params,omitempty"`
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID any `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type MCPResponse struct {
|
||||
|
||||
@@ -15,14 +15,14 @@ import (
|
||||
|
||||
// PagesConfigResponse represents the pages configuration for a repository
|
||||
type PagesConfigResponse struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PublicLanding bool `json:"public_landing"`
|
||||
Template string `json:"template"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Branding pages_module.BrandingConfig `json:"branding"`
|
||||
Hero pages_module.HeroConfig `json:"hero"`
|
||||
SEO pages_module.SEOConfig `json:"seo"`
|
||||
Footer pages_module.FooterConfig `json:"footer"`
|
||||
Enabled bool `json:"enabled"`
|
||||
PublicLanding bool `json:"public_landing"`
|
||||
Template string `json:"template"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Brand pages_module.BrandConfig `json:"brand"`
|
||||
Hero pages_module.HeroConfig `json:"hero"`
|
||||
SEO pages_module.SEOConfig `json:"seo"`
|
||||
Footer pages_module.FooterConfig `json:"footer"`
|
||||
}
|
||||
|
||||
// PagesContentResponse represents the rendered content for a landing page
|
||||
@@ -52,7 +52,7 @@ func GetPagesConfig(ctx *context.APIContext) {
|
||||
PublicLanding: config.PublicLanding,
|
||||
Template: config.Template,
|
||||
Domain: config.Domain,
|
||||
Branding: config.Branding,
|
||||
Brand: config.Brand,
|
||||
Hero: config.Hero,
|
||||
SEO: config.SEO,
|
||||
Footer: config.Footer,
|
||||
@@ -82,7 +82,10 @@ func GetPagesContent(ctx *context.APIContext) {
|
||||
// Build title
|
||||
title := config.SEO.Title
|
||||
if title == "" {
|
||||
title = config.Hero.Title
|
||||
title = config.Hero.Headline
|
||||
}
|
||||
if title == "" {
|
||||
title = config.Brand.Name
|
||||
}
|
||||
if title == "" {
|
||||
title = repo.Name
|
||||
@@ -91,7 +94,7 @@ func GetPagesContent(ctx *context.APIContext) {
|
||||
// Build description
|
||||
description := config.SEO.Description
|
||||
if description == "" {
|
||||
description = config.Hero.Tagline
|
||||
description = config.Hero.Subheadline
|
||||
}
|
||||
if description == "" {
|
||||
description = repo.Description
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -235,6 +238,14 @@ func ChangeConfig(ctx *context.Context) {
|
||||
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
|
||||
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
|
||||
cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()),
|
||||
cfg.Theme.DisableRegistration.DynKey(): marshalBool,
|
||||
cfg.Theme.CustomHomeHTML.DynKey(): marshalString(""),
|
||||
cfg.Theme.APIHeaderURL.DynKey(): marshalString(""),
|
||||
cfg.Theme.HelpURL.DynKey(): marshalString(""),
|
||||
cfg.Theme.CustomHomeTitle.DynKey(): marshalString(""),
|
||||
cfg.Theme.CustomHomeTagline.DynKey(): marshalString(""),
|
||||
cfg.Theme.PinnedOrgDisplayFormat.DynKey(): marshalString("condensed"),
|
||||
cfg.Theme.ExploreOrgDisplayFormat.DynKey(): marshalString("list"),
|
||||
}
|
||||
|
||||
_ = ctx.Req.ParseForm()
|
||||
@@ -272,3 +283,167 @@ loop:
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// ChangeThemeLogo handles homepage logo upload and custom URL
|
||||
func ChangeThemeLogo(ctx *context.Context) {
|
||||
cfg := setting.Config()
|
||||
|
||||
action := ctx.FormString("action")
|
||||
if action == "reset" {
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomHomeLogoURL.DynKey(): "\"\"",
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.logo_reset_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for file upload first
|
||||
file, header, err := ctx.Req.FormFile("logo_file")
|
||||
if err == nil && header != nil {
|
||||
defer file.Close()
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
allowedExts := map[string]bool{".svg": true, ".png": true, ".jpg": true, ".jpeg": true, ".gif": true}
|
||||
if !allowedExts[ext] {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.logo_invalid_type"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
customDir := filepath.Join(setting.CustomPath, "public", "assets", "img")
|
||||
if err := os.MkdirAll(customDir, 0o755); err != nil {
|
||||
ctx.ServerError("MkdirAll", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := "custom-home-logo" + ext
|
||||
filePath := filepath.Join(customDir, fileName)
|
||||
destFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("Create", err)
|
||||
return
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
if _, err := io.Copy(destFile, file); err != nil {
|
||||
ctx.ServerError("Copy", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileURL := setting.AppSubURL + "/assets/img/" + fileName
|
||||
marshaledValue, _ := json.Marshal(fileURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomHomeLogoURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.logo_upload_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for custom URL
|
||||
customURL := ctx.FormString("custom_logo_url")
|
||||
marshaledValue, _ := json.Marshal(customURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomHomeLogoURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.logo_url_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
}
|
||||
|
||||
// ChangeThemeIcon handles site icon (favicon + navbar) upload and custom URL
|
||||
func ChangeThemeIcon(ctx *context.Context) {
|
||||
cfg := setting.Config()
|
||||
|
||||
action := ctx.FormString("action")
|
||||
if action == "reset" {
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomSiteIconURL.DynKey(): "\"\"",
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.icon_reset_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for file upload first
|
||||
file, header, err := ctx.Req.FormFile("icon_file")
|
||||
if err == nil && header != nil {
|
||||
defer file.Close()
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
allowedExts := map[string]bool{".svg": true, ".png": true, ".ico": true}
|
||||
if !allowedExts[ext] {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.icon_invalid_type"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
customDir := filepath.Join(setting.CustomPath, "public", "assets", "img")
|
||||
if err := os.MkdirAll(customDir, 0o755); err != nil {
|
||||
ctx.ServerError("MkdirAll", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := "custom-site-icon" + ext
|
||||
filePath := filepath.Join(customDir, fileName)
|
||||
destFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("Create", err)
|
||||
return
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
if _, err := io.Copy(destFile, file); err != nil {
|
||||
ctx.ServerError("Copy", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileURL := setting.AppSubURL + "/assets/img/" + fileName
|
||||
marshaledValue, _ := json.Marshal(fileURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomSiteIconURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.icon_upload_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for custom URL
|
||||
customURL := ctx.FormString("custom_icon_url")
|
||||
marshaledValue, _ := json.Marshal(customURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomSiteIconURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.icon_url_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func Code(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage || setting.Config().Theme.HideExploreUsers.Value(ctx)
|
||||
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
ctx.Data["Title"] = ctx.Tr("explore_title")
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
@@ -20,7 +19,7 @@ func Organizations(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage || setting.Config().Theme.HideExploreUsers.Value(ctx)
|
||||
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
||||
ctx.Data["Title"] = ctx.Tr("explore_title")
|
||||
ctx.Data["PageIsExplore"] = true
|
||||
@@ -40,7 +39,7 @@ func Organizations(ctx *context.Context) {
|
||||
)
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if sortOrder == "" {
|
||||
sortOrder = util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest")
|
||||
sortOrder = "alphabetically"
|
||||
ctx.SetFormString("sort", sortOrder)
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
|
||||
|
||||
// Repos render explore repositories page
|
||||
func Repos(ctx *context.Context) {
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage || setting.Config().Theme.HideExploreUsers.Value(ctx)
|
||||
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
||||
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
||||
ctx.Data["Title"] = ctx.Tr("explore_title")
|
||||
|
||||
@@ -128,7 +128,7 @@ func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, t
|
||||
|
||||
// Users render explore users page
|
||||
func Users(ctx *context.Context) {
|
||||
if setting.Service.Explore.DisableUsersPage {
|
||||
if setting.Service.Explore.DisableUsersPage || setting.Config().Theme.HideExploreUsers.Value(ctx) {
|
||||
ctx.Redirect(setting.AppSubURL + "/explore")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
organization_model "code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -61,6 +62,14 @@ func Home(ctx *context.Context) {
|
||||
|
||||
ctx.Data["PageIsHome"] = true
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
|
||||
// Load pinned organizations for homepage
|
||||
pinnedOrgs, err := organization_model.GetHomepagePinnedOrganizations(ctx)
|
||||
if err != nil {
|
||||
log.Error("GetHomepagePinnedOrganizations: %v", err)
|
||||
} else {
|
||||
ctx.Data["PinnedOrganizations"] = pinnedOrgs
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplHome)
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,15 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
ctx.Data["UngroupedPinned"] = ungroupedPinned
|
||||
ctx.Data["HasPinnedRepos"] = len(pinnedRepos) > 0
|
||||
|
||||
// Count public pinned repos for non-member visibility
|
||||
hasPublicPinnedRepos := false
|
||||
for _, p := range pinnedRepos {
|
||||
if repo, ok := p.Repo.(*repo_model.Repository); ok && repo != nil && !repo.IsPrivate {
|
||||
hasPublicPinnedRepos = true
|
||||
break
|
||||
}
|
||||
}
|
||||
ctx.Data["HasPublicPinnedRepos"] = hasPublicPinnedRepos
|
||||
// Load public members (limit to 12 for overview display)
|
||||
publicMembers, totalPublicMembers, err := organization.GetPublicOrgMembers(ctx, org.ID, 12)
|
||||
if err != nil {
|
||||
|
||||
@@ -45,6 +45,7 @@ func Settings(ctx *context.Context) {
|
||||
ctx.Data["PageIsSettingsOptions"] = true
|
||||
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
|
||||
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
|
||||
ctx.Data["IsHomepagePinned"] = ctx.Org.Organization.IsHomepagePinned
|
||||
ctx.Data["ContextUser"] = ctx.ContextUser
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
@@ -89,6 +90,14 @@ func SettingsPost(ctx *context.Context) {
|
||||
opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
|
||||
}
|
||||
|
||||
// Handle homepage pinning (admin only)
|
||||
if ctx.Doer.IsAdmin {
|
||||
if err := org.SetHomepagePinned(ctx, form.IsHomepagePinned); err != nil {
|
||||
ctx.ServerError("SetHomepagePinned", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
@@ -22,10 +23,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
tplPagesSimple templates.TplName = "pages/simple"
|
||||
tplPagesDocumentation templates.TplName = "pages/documentation"
|
||||
tplPagesProduct templates.TplName = "pages/product"
|
||||
tplPagesPortfolio templates.TplName = "pages/portfolio"
|
||||
tplPagesOpenSourceHero templates.TplName = "pages/open-source-hero"
|
||||
tplPagesMinimalistDocs templates.TplName = "pages/minimalist-docs"
|
||||
tplPagesSaasConversion templates.TplName = "pages/saas-conversion"
|
||||
tplPagesBoldMarketing templates.TplName = "pages/bold-marketing"
|
||||
)
|
||||
|
||||
// ServeLandingPage serves the landing page for a repository
|
||||
@@ -106,7 +107,7 @@ func renderLandingPage(ctx *context.Context, repo *repo_model.Repository, config
|
||||
ctx.Data["PageIsPagesLanding"] = true
|
||||
|
||||
// Load README content
|
||||
readme, err := loadReadmeContent(ctx, repo, config)
|
||||
readme, err := loadReadmeContent(ctx, repo)
|
||||
if err != nil {
|
||||
log.Warn("Failed to load README: %v", err)
|
||||
}
|
||||
@@ -115,6 +116,13 @@ func renderLandingPage(ctx *context.Context, repo *repo_model.Repository, config
|
||||
// Load repo stats
|
||||
ctx.Data["NumStars"] = repo.NumStars
|
||||
ctx.Data["NumForks"] = repo.NumForks
|
||||
ctx.Data["Year"] = time.Now().Year()
|
||||
|
||||
// Load latest release
|
||||
release, err := repo_model.GetLatestReleaseByRepoID(ctx, repo.ID)
|
||||
if err == nil && release != nil {
|
||||
ctx.Data["LatestRelease"] = release
|
||||
}
|
||||
|
||||
// Select template based on config
|
||||
tpl := selectTemplate(config.Template)
|
||||
@@ -127,14 +135,17 @@ func getPageTitle(repo *repo_model.Repository, config *pages_module.LandingConfi
|
||||
if config.SEO.Title != "" {
|
||||
return config.SEO.Title
|
||||
}
|
||||
if config.Hero.Title != "" {
|
||||
return config.Hero.Title
|
||||
if config.Hero.Headline != "" {
|
||||
return config.Hero.Headline
|
||||
}
|
||||
if config.Brand.Name != "" {
|
||||
return config.Brand.Name
|
||||
}
|
||||
return repo.Name
|
||||
}
|
||||
|
||||
// loadReadmeContent loads and renders the README content
|
||||
func loadReadmeContent(ctx *context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig) (template.HTML, error) {
|
||||
func loadReadmeContent(ctx *context.Context, repo *repo_model.Repository) (template.HTML, error) {
|
||||
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -152,7 +163,7 @@ func loadReadmeContent(ctx *context.Context, repo *repo_model.Repository, config
|
||||
}
|
||||
|
||||
// Find README file
|
||||
readmePath := findReadmePath(commit, config)
|
||||
readmePath := findReadmePath(commit)
|
||||
if readmePath == "" {
|
||||
return "", errors.New("README not found")
|
||||
}
|
||||
@@ -185,14 +196,7 @@ func loadReadmeContent(ctx *context.Context, repo *repo_model.Repository, config
|
||||
}
|
||||
|
||||
// findReadmePath finds the README file path
|
||||
func findReadmePath(commit *git.Commit, config *pages_module.LandingConfig) string {
|
||||
// Check config for custom readme location
|
||||
for _, section := range config.Sections {
|
||||
if section.Type == "readme" && section.File != "" {
|
||||
return section.File
|
||||
}
|
||||
}
|
||||
|
||||
func findReadmePath(commit *git.Commit) string {
|
||||
// Default README locations
|
||||
readmePaths := []string{
|
||||
"README.md",
|
||||
@@ -215,14 +219,16 @@ func findReadmePath(commit *git.Commit, config *pages_module.LandingConfig) stri
|
||||
// selectTemplate selects the template based on configuration
|
||||
func selectTemplate(templateName string) templates.TplName {
|
||||
switch templateName {
|
||||
case "documentation":
|
||||
return tplPagesDocumentation
|
||||
case "product":
|
||||
return tplPagesProduct
|
||||
case "portfolio":
|
||||
return tplPagesPortfolio
|
||||
case "minimalist-docs":
|
||||
return tplPagesMinimalistDocs
|
||||
case "saas-conversion":
|
||||
return tplPagesSaasConversion
|
||||
case "bold-marketing":
|
||||
return tplPagesBoldMarketing
|
||||
case "open-source-hero":
|
||||
return tplPagesOpenSourceHero
|
||||
default:
|
||||
return tplPagesSimple
|
||||
return tplPagesOpenSourceHero
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
155
routers/web/repo/setting/license.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
const tplLicense templates.TplName = "repo/settings/license"
|
||||
|
||||
// LicenseInfo holds license information
|
||||
type LicenseInfo struct {
|
||||
Key string
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// LicenseCategory holds a category of licenses
|
||||
type LicenseCategory struct {
|
||||
Category string
|
||||
Licenses []LicenseInfo
|
||||
}
|
||||
|
||||
// LicenseTypes defines all available license types
|
||||
var LicenseTypes = []LicenseCategory{
|
||||
{
|
||||
Category: "Permissive Licenses",
|
||||
Licenses: []LicenseInfo{
|
||||
{"MIT", "MIT License", "Most popular overall, minimal restrictions"},
|
||||
{"Apache-2.0", "Apache License 2.0", "Like MIT, plus explicit patent protection"},
|
||||
{"BSD-2-Clause", "BSD 2-Clause", "Similar to MIT"},
|
||||
{"BSD-3-Clause", "BSD 3-Clause", "BSD with no-endorsement rule"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Category: "Weak Copyleft Licenses",
|
||||
Licenses: []LicenseInfo{
|
||||
{"MPL-2.0", "Mozilla Public License 2.0", "Only modified files must remain open source"},
|
||||
{"LGPL-2.1", "LGPL 2.1", "Allows linking from proprietary software"},
|
||||
{"LGPL-3.0", "LGPL 3.0", "Allows linking from proprietary software"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Category: "Strong Copyleft Licenses",
|
||||
Licenses: []LicenseInfo{
|
||||
{"GPL-2.0", "GNU GPL v2", "Derivatives must also be GPL"},
|
||||
{"GPL-3.0", "GNU GPL v3", "Derivatives must be GPL, includes patent clauses"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Category: "Network/SaaS Copyleft",
|
||||
Licenses: []LicenseInfo{
|
||||
{"AGPL-3.0", "AGPL v3", "Like GPL, but closes the SaaS loophole"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Category: "Source-Available",
|
||||
Licenses: []LicenseInfo{
|
||||
{"BSL-1.0", "Business Source License", "Free to use, becomes open source later"},
|
||||
{"SSPL-1.0", "Server Side Public License", "Requires publishing entire service stack"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Category: "Public Domain",
|
||||
Licenses: []LicenseInfo{
|
||||
{"Unlicense", "Unlicense", "Public domain, no attribution required"},
|
||||
{"CC0-1.0", "CC0", "Public domain, internationally defensible"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// License shows the license settings page
|
||||
func License(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.license")
|
||||
ctx.Data["PageIsSettingsLicense"] = true
|
||||
ctx.Data["LicenseTypes"] = LicenseTypes
|
||||
ctx.HTML(http.StatusOK, tplLicense)
|
||||
}
|
||||
|
||||
// LicensePost handles license settings form submission
|
||||
func LicensePost(ctx *context.Context) {
|
||||
licenseType := ctx.FormString("license_type")
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
oldLicenseType := repo.LicenseType
|
||||
|
||||
// Update repo license type
|
||||
repo.LicenseType = licenseType
|
||||
if err := repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "license_type"); err != nil {
|
||||
ctx.ServerError("UpdateRepositoryCols", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create LICENSE.md file if license selected and different from before
|
||||
if licenseType != "" && licenseType != oldLicenseType {
|
||||
if err := createLicenseFile(ctx, repo, licenseType); err != nil {
|
||||
log.Error("Failed to create LICENSE.md: %v", err)
|
||||
ctx.Flash.Warning(ctx.Tr("repo.settings.license_file_error"))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.license_saved"))
|
||||
}
|
||||
} else if licenseType == "" {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.license_cleared"))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.license_saved"))
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/license")
|
||||
}
|
||||
|
||||
func createLicenseFile(ctx *context.Context, repo *repo_model.Repository, licenseType string) error {
|
||||
// Get license content from templates
|
||||
licenseContent, err := repo_module.GetLicense(licenseType, &repo_module.LicenseValues{
|
||||
Owner: repo.OwnerName,
|
||||
Email: ctx.Doer.Email,
|
||||
Repo: repo.Name,
|
||||
Year: time.Now().Format("2006"),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetLicense: %w", err)
|
||||
}
|
||||
|
||||
// Create/update LICENSE.md using files service
|
||||
opts := &files_service.ChangeRepoFilesOptions{
|
||||
Message: fmt.Sprintf("Add LICENSE.md (%s)", licenseType),
|
||||
OldBranch: repo.DefaultBranch,
|
||||
NewBranch: repo.DefaultBranch,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "LICENSE.md",
|
||||
ContentReader: bytes.NewReader(licenseContent),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = files_service.ChangeRepoFiles(ctx, repo, ctx.Doer, opts)
|
||||
if err != nil {
|
||||
// If file already exists, try to update it instead
|
||||
opts.Files[0].Operation = "update"
|
||||
_, err = files_service.ChangeRepoFiles(ctx, repo, ctx.Doer, opts)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -4,23 +4,98 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
pages_module "code.gitea.io/gitea/modules/pages"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
pages_service "code.gitea.io/gitea/services/pages"
|
||||
)
|
||||
|
||||
const tplRepoSettingsPages templates.TplName = "repo/settings/pages"
|
||||
const (
|
||||
tplRepoSettingsPages templates.TplName = "repo/settings/pages"
|
||||
tplRepoSettingsPagesBrand templates.TplName = "repo/settings/pages_brand"
|
||||
tplRepoSettingsPagesHero templates.TplName = "repo/settings/pages_hero"
|
||||
tplRepoSettingsPagesContent templates.TplName = "repo/settings/pages_content"
|
||||
tplRepoSettingsPagesSocial templates.TplName = "repo/settings/pages_social"
|
||||
tplRepoSettingsPagesPricing templates.TplName = "repo/settings/pages_pricing"
|
||||
tplRepoSettingsPagesFooter templates.TplName = "repo/settings/pages_footer"
|
||||
tplRepoSettingsPagesTheme templates.TplName = "repo/settings/pages_theme"
|
||||
)
|
||||
|
||||
// Pages shows the repository pages settings
|
||||
// getPagesLandingConfig loads the landing page configuration
|
||||
func getPagesLandingConfig(ctx *context.Context) *pages_module.LandingConfig {
|
||||
config, err := pages_service.GetPagesConfig(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
// Return default config
|
||||
return &pages_module.LandingConfig{
|
||||
Enabled: false,
|
||||
Template: "open-source-hero",
|
||||
Brand: pages_module.BrandConfig{Name: ctx.Repo.Repository.Name},
|
||||
Hero: pages_module.HeroConfig{
|
||||
Headline: ctx.Repo.Repository.Name,
|
||||
Subheadline: ctx.Repo.Repository.Description,
|
||||
},
|
||||
Theme: pages_module.ThemeConfig{Mode: "auto"},
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// savePagesLandingConfig saves the landing page configuration
|
||||
func savePagesLandingConfig(ctx *context.Context, config *pages_module.LandingConfig) error {
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbConfig, err := repo_model.GetPagesConfigByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
if repo_model.IsErrPagesConfigNotExist(err) {
|
||||
// Create new config
|
||||
dbConfig = &repo_model.PagesConfig{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Enabled: config.Enabled,
|
||||
Template: repo_model.PagesTemplate(config.Template),
|
||||
ConfigJSON: string(configJSON),
|
||||
}
|
||||
return repo_model.CreatePagesConfig(ctx, dbConfig)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Update existing config
|
||||
dbConfig.Enabled = config.Enabled
|
||||
dbConfig.Template = repo_model.PagesTemplate(config.Template)
|
||||
dbConfig.ConfigJSON = string(configJSON)
|
||||
return repo_model.UpdatePagesConfig(ctx, dbConfig)
|
||||
}
|
||||
|
||||
// setCommonPagesData sets common data for all pages settings pages
|
||||
func setCommonPagesData(ctx *context.Context) {
|
||||
config := getPagesLandingConfig(ctx)
|
||||
ctx.Data["Config"] = config
|
||||
ctx.Data["PagesEnabled"] = config.Enabled
|
||||
ctx.Data["PagesSubdomain"] = pages_service.GetPagesSubdomain(ctx.Repo.Repository)
|
||||
ctx.Data["PagesURL"] = pages_service.GetPagesURL(ctx.Repo.Repository)
|
||||
ctx.Data["PagesTemplates"] = pages_module.ValidTemplates()
|
||||
ctx.Data["PagesTemplateNames"] = pages_module.TemplateDisplayNames()
|
||||
}
|
||||
|
||||
// Pages shows the repository pages settings (General page)
|
||||
func Pages(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.pages")
|
||||
ctx.Data["PageIsSettingsPages"] = true
|
||||
ctx.Data["PageIsSettingsPagesGeneral"] = true
|
||||
|
||||
setCommonPagesData(ctx)
|
||||
|
||||
// Get pages config
|
||||
config, err := repo_model.GetPagesConfig(ctx, ctx.Repo.Repository.ID)
|
||||
config, err := repo_model.GetPagesConfigByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil && !repo_model.IsErrPagesConfigNotExist(err) {
|
||||
ctx.ServerError("GetPagesConfig", err)
|
||||
return
|
||||
@@ -39,53 +114,45 @@ func Pages(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["PagesDomains"] = domains
|
||||
|
||||
// Generate subdomain
|
||||
ctx.Data["PagesSubdomain"] = pages_service.GetPagesSubdomain(ctx.Repo.Repository)
|
||||
ctx.Data["PagesURL"] = pages_service.GetPagesURL(ctx.Repo.Repository)
|
||||
|
||||
// Available templates
|
||||
ctx.Data["PagesTemplates"] = []string{"simple", "documentation", "product", "portfolio"}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplRepoSettingsPages)
|
||||
}
|
||||
|
||||
// PagesPost handles the pages settings form submission
|
||||
func PagesPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.pages")
|
||||
ctx.Data["PageIsSettingsPages"] = true
|
||||
|
||||
action := ctx.FormString("action")
|
||||
|
||||
switch action {
|
||||
case "enable":
|
||||
template := ctx.FormString("template")
|
||||
if template == "" {
|
||||
template = "simple"
|
||||
if template == "" || !pages_module.IsValidTemplate(template) {
|
||||
template = "open-source-hero"
|
||||
}
|
||||
if err := pages_service.EnablePages(ctx, ctx.Repo.Repository, template); err != nil {
|
||||
config := getPagesLandingConfig(ctx)
|
||||
config.Enabled = true
|
||||
config.Template = template
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("EnablePages", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.enabled_success"))
|
||||
|
||||
case "disable":
|
||||
if err := pages_service.DisablePages(ctx, ctx.Repo.Repository); err != nil {
|
||||
config := getPagesLandingConfig(ctx)
|
||||
config.Enabled = false
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("DisablePages", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.disabled_success"))
|
||||
|
||||
case "update_template":
|
||||
template := ctx.FormString("template")
|
||||
if template == "" {
|
||||
template = "simple"
|
||||
if template == "" || !pages_module.IsValidTemplate(template) {
|
||||
template = "open-source-hero"
|
||||
}
|
||||
if err := pages_service.EnablePages(ctx, ctx.Repo.Repository, template); err != nil {
|
||||
ctx.ServerError("EnablePages", err)
|
||||
config := getPagesLandingConfig(ctx)
|
||||
config.Template = template
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("UpdateTemplate", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
|
||||
case "add_domain":
|
||||
domain := ctx.FormString("domain")
|
||||
if domain == "" {
|
||||
@@ -105,7 +172,6 @@ func PagesPost(ctx *context.Context) {
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.domain_added"))
|
||||
}
|
||||
|
||||
case "delete_domain":
|
||||
domainID := ctx.FormInt64("domain_id")
|
||||
if err := repo_model.DeletePagesDomain(ctx, domainID); err != nil {
|
||||
@@ -113,7 +179,6 @@ func PagesPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.domain_deleted"))
|
||||
|
||||
case "activate_ssl":
|
||||
domainID := ctx.FormInt64("domain_id")
|
||||
if err := repo_model.ActivatePagesDomainSSL(ctx, domainID); err != nil {
|
||||
@@ -121,24 +186,264 @@ func PagesPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.ssl_activated"))
|
||||
|
||||
case "verify_domain":
|
||||
domainID := ctx.FormInt64("domain_id")
|
||||
if err := pages_service.VerifyDomain(ctx, domainID); err != nil {
|
||||
if err.Error() == "DNS verification failed" {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.pages.domain_verification_failed"))
|
||||
} else {
|
||||
ctx.ServerError("VerifyDomain", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.pages.domain_verification_failed"))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.domain_verified"))
|
||||
}
|
||||
|
||||
default:
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages")
|
||||
}
|
||||
|
||||
func PagesBrand(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.brand")
|
||||
ctx.Data["PageIsSettingsPages"] = true
|
||||
ctx.Data["PageIsSettingsPagesBrand"] = true
|
||||
setCommonPagesData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplRepoSettingsPagesBrand)
|
||||
}
|
||||
|
||||
func PagesBrandPost(ctx *context.Context) {
|
||||
config := getPagesLandingConfig(ctx)
|
||||
config.Brand.Name = ctx.FormString("brand_name")
|
||||
config.Brand.LogoURL = ctx.FormString("brand_logo_url")
|
||||
config.Brand.Tagline = ctx.FormString("brand_tagline")
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("SavePagesConfig", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/brand")
|
||||
}
|
||||
|
||||
func PagesHero(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.hero")
|
||||
ctx.Data["PageIsSettingsPages"] = true
|
||||
ctx.Data["PageIsSettingsPagesHero"] = true
|
||||
setCommonPagesData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplRepoSettingsPagesHero)
|
||||
}
|
||||
|
||||
func PagesHeroPost(ctx *context.Context) {
|
||||
config := getPagesLandingConfig(ctx)
|
||||
config.Hero.Headline = ctx.FormString("headline")
|
||||
config.Hero.Subheadline = ctx.FormString("subheadline")
|
||||
config.Hero.ImageURL = ctx.FormString("image_url")
|
||||
config.Hero.VideoURL = ctx.FormString("video_url")
|
||||
config.Hero.CodeExample = ctx.FormString("code_example")
|
||||
config.Hero.PrimaryCTA.Label = ctx.FormString("primary_cta_label")
|
||||
config.Hero.PrimaryCTA.URL = ctx.FormString("primary_cta_url")
|
||||
config.Hero.PrimaryCTA.Variant = ctx.FormString("primary_cta_variant")
|
||||
config.Hero.SecondaryCTA.Label = ctx.FormString("secondary_cta_label")
|
||||
config.Hero.SecondaryCTA.URL = ctx.FormString("secondary_cta_url")
|
||||
config.Hero.SecondaryCTA.Variant = ctx.FormString("secondary_cta_variant")
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("SavePagesConfig", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/hero")
|
||||
}
|
||||
|
||||
func PagesContent(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.content")
|
||||
ctx.Data["PageIsSettingsPages"] = true
|
||||
ctx.Data["PageIsSettingsPagesContent"] = true
|
||||
setCommonPagesData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplRepoSettingsPagesContent)
|
||||
}
|
||||
|
||||
func PagesContentPost(ctx *context.Context) {
|
||||
config := getPagesLandingConfig(ctx)
|
||||
config.Stats = nil
|
||||
for i := range 10 {
|
||||
value := ctx.FormString(fmt.Sprintf("stat_value_%d", i))
|
||||
label := ctx.FormString(fmt.Sprintf("stat_label_%d", i))
|
||||
if value == "" && label == "" {
|
||||
continue
|
||||
}
|
||||
config.Stats = append(config.Stats, pages_module.StatConfig{Value: value, Label: label})
|
||||
}
|
||||
config.ValueProps = nil
|
||||
for i := range 10 {
|
||||
title := ctx.FormString(fmt.Sprintf("valueprop_title_%d", i))
|
||||
desc := ctx.FormString(fmt.Sprintf("valueprop_desc_%d", i))
|
||||
icon := ctx.FormString(fmt.Sprintf("valueprop_icon_%d", i))
|
||||
if title == "" && desc == "" {
|
||||
continue
|
||||
}
|
||||
config.ValueProps = append(config.ValueProps, pages_module.ValuePropConfig{Title: title, Description: desc, Icon: icon})
|
||||
}
|
||||
config.Features = nil
|
||||
for i := range 20 {
|
||||
title := ctx.FormString(fmt.Sprintf("feature_title_%d", i))
|
||||
desc := ctx.FormString(fmt.Sprintf("feature_desc_%d", i))
|
||||
icon := ctx.FormString(fmt.Sprintf("feature_icon_%d", i))
|
||||
imageURL := ctx.FormString(fmt.Sprintf("feature_image_%d", i))
|
||||
if title == "" && desc == "" {
|
||||
continue
|
||||
}
|
||||
config.Features = append(config.Features, pages_module.FeatureConfig{Title: title, Description: desc, Icon: icon, ImageURL: imageURL})
|
||||
}
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("SavePagesConfig", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/content")
|
||||
}
|
||||
|
||||
func PagesSocial(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.social")
|
||||
ctx.Data["PageIsSettingsPages"] = true
|
||||
ctx.Data["PageIsSettingsPagesSocial"] = true
|
||||
setCommonPagesData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplRepoSettingsPagesSocial)
|
||||
}
|
||||
|
||||
func PagesSocialPost(ctx *context.Context) {
|
||||
config := getPagesLandingConfig(ctx)
|
||||
config.SocialProof.Logos = nil
|
||||
for i := range 20 {
|
||||
logo := ctx.FormString(fmt.Sprintf("logo_%d", i))
|
||||
if logo == "" {
|
||||
continue
|
||||
}
|
||||
config.SocialProof.Logos = append(config.SocialProof.Logos, logo)
|
||||
}
|
||||
config.SocialProof.Testimonials = nil
|
||||
for i := range 10 {
|
||||
quote := ctx.FormString(fmt.Sprintf("testimonial_quote_%d", i))
|
||||
author := ctx.FormString(fmt.Sprintf("testimonial_author_%d", i))
|
||||
role := ctx.FormString(fmt.Sprintf("testimonial_role_%d", i))
|
||||
avatar := ctx.FormString(fmt.Sprintf("testimonial_avatar_%d", i))
|
||||
if quote == "" && author == "" {
|
||||
continue
|
||||
}
|
||||
config.SocialProof.Testimonials = append(config.SocialProof.Testimonials, pages_module.TestimonialConfig{Quote: quote, Author: author, Role: role, Avatar: avatar})
|
||||
}
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("SavePagesConfig", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/social")
|
||||
}
|
||||
|
||||
func PagesPricing(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.pricing")
|
||||
ctx.Data["PageIsSettingsPages"] = true
|
||||
ctx.Data["PageIsSettingsPagesPricing"] = true
|
||||
setCommonPagesData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplRepoSettingsPagesPricing)
|
||||
}
|
||||
|
||||
func PagesPricingPost(ctx *context.Context) {
|
||||
config := getPagesLandingConfig(ctx)
|
||||
config.Pricing.Headline = ctx.FormString("pricing_headline")
|
||||
config.Pricing.Subheadline = ctx.FormString("pricing_subheadline")
|
||||
config.Pricing.Plans = nil
|
||||
for i := range 5 {
|
||||
name := ctx.FormString(fmt.Sprintf("plan_name_%d", i))
|
||||
price := ctx.FormString(fmt.Sprintf("plan_price_%d", i))
|
||||
if name == "" && price == "" {
|
||||
continue
|
||||
}
|
||||
featuresText := ctx.FormString(fmt.Sprintf("plan_%d_features", i))
|
||||
var features []string
|
||||
for f := range strings.SplitSeq(featuresText, "\n") {
|
||||
f = strings.TrimSpace(f)
|
||||
if f != "" {
|
||||
features = append(features, f)
|
||||
}
|
||||
}
|
||||
config.Pricing.Plans = append(config.Pricing.Plans, pages_module.PricingPlanConfig{
|
||||
Name: name, Price: price, Period: ctx.FormString(fmt.Sprintf("plan_period_%d", i)),
|
||||
Features: features, CTA: ctx.FormString(fmt.Sprintf("plan_cta_%d", i)),
|
||||
Featured: ctx.FormBool(fmt.Sprintf("plan_featured_%d", i)),
|
||||
})
|
||||
}
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("SavePagesConfig", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/pricing")
|
||||
}
|
||||
|
||||
func PagesFooter(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.footer")
|
||||
ctx.Data["PageIsSettingsPages"] = true
|
||||
ctx.Data["PageIsSettingsPagesFooter"] = true
|
||||
setCommonPagesData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplRepoSettingsPagesFooter)
|
||||
}
|
||||
|
||||
func PagesFooterPost(ctx *context.Context) {
|
||||
config := getPagesLandingConfig(ctx)
|
||||
config.CTASection.Headline = ctx.FormString("cta_headline")
|
||||
config.CTASection.Subheadline = ctx.FormString("cta_subheadline")
|
||||
config.CTASection.Button.Label = ctx.FormString("cta_button_label")
|
||||
config.CTASection.Button.URL = ctx.FormString("cta_button_url")
|
||||
config.CTASection.Button.Variant = ctx.FormString("cta_button_variant")
|
||||
config.Footer.Copyright = ctx.FormString("footer_copyright")
|
||||
config.Footer.Links = nil
|
||||
for i := range 10 {
|
||||
label := ctx.FormString(fmt.Sprintf("footer_link_label_%d", i))
|
||||
url := ctx.FormString(fmt.Sprintf("footer_link_url_%d", i))
|
||||
if label == "" && url == "" {
|
||||
continue
|
||||
}
|
||||
config.Footer.Links = append(config.Footer.Links, pages_module.FooterLink{Label: label, URL: url})
|
||||
}
|
||||
config.Footer.Social = nil
|
||||
for i := range 10 {
|
||||
platform := ctx.FormString(fmt.Sprintf("social_platform_%d", i))
|
||||
url := ctx.FormString(fmt.Sprintf("social_url_%d", i))
|
||||
if platform == "" && url == "" {
|
||||
continue
|
||||
}
|
||||
config.Footer.Social = append(config.Footer.Social, pages_module.SocialLink{Platform: platform, URL: url})
|
||||
}
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("SavePagesConfig", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/footer")
|
||||
}
|
||||
|
||||
func PagesTheme(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.theme")
|
||||
ctx.Data["PageIsSettingsPages"] = true
|
||||
ctx.Data["PageIsSettingsPagesTheme"] = true
|
||||
setCommonPagesData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplRepoSettingsPagesTheme)
|
||||
}
|
||||
|
||||
func PagesThemePost(ctx *context.Context) {
|
||||
config := getPagesLandingConfig(ctx)
|
||||
config.Theme.PrimaryColor = ctx.FormString("primary_color")
|
||||
config.Theme.AccentColor = ctx.FormString("accent_color")
|
||||
config.Theme.Mode = ctx.FormString("theme_mode")
|
||||
config.SEO.Title = ctx.FormString("seo_title")
|
||||
config.SEO.Description = ctx.FormString("seo_description")
|
||||
keywords := ctx.FormString("seo_keywords")
|
||||
if keywords != "" {
|
||||
config.SEO.Keywords = strings.Split(keywords, ",")
|
||||
} else {
|
||||
config.SEO.Keywords = nil
|
||||
}
|
||||
config.SEO.OGImage = ctx.FormString("og_image")
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("SavePagesConfig", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/theme")
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ func handleSettingsPostUpdate(ctx *context.Context) {
|
||||
repo.Name = newRepoName
|
||||
repo.LowerName = strings.ToLower(newRepoName)
|
||||
repo.Description = form.Description
|
||||
repo.DisplayTitle = form.DisplayTitle
|
||||
repo.Website = form.Website
|
||||
repo.IsTemplate = form.Template
|
||||
|
||||
|
||||
@@ -778,6 +778,8 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/test_mail", admin.SendTestMail)
|
||||
m.Post("/test_cache", admin.TestCache)
|
||||
m.Get("/settings", admin.ConfigSettings)
|
||||
m.Post("/theme/logo", admin.ChangeThemeLogo)
|
||||
m.Post("/theme/icon", admin.ChangeThemeIcon)
|
||||
})
|
||||
|
||||
m.Group("/monitor", func() {
|
||||
@@ -1138,6 +1140,11 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/avatar", web.Bind(forms.AvatarForm{}), repo_setting.SettingsAvatar)
|
||||
m.Post("/avatar/delete", repo_setting.SettingsDeleteAvatar)
|
||||
|
||||
m.Group("/license", func() {
|
||||
m.Get("", repo_setting.License)
|
||||
m.Post("", repo_setting.LicensePost)
|
||||
})
|
||||
|
||||
m.Combo("/public_access").Get(repo_setting.PublicAccess).Post(repo_setting.PublicAccessPost)
|
||||
|
||||
m.Group("/collaboration", func() {
|
||||
@@ -1207,7 +1214,16 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/{lid}/unlock", repo_setting.LFSUnlock)
|
||||
})
|
||||
})
|
||||
m.Combo("/pages").Get(repo_setting.Pages).Post(repo_setting.PagesPost)
|
||||
m.Group("/pages", func() {
|
||||
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.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)
|
||||
m.Combo("/footer").Get(repo_setting.PagesFooter).Post(repo_setting.PagesFooterPost)
|
||||
m.Combo("/theme").Get(repo_setting.PagesTheme).Post(repo_setting.PagesThemePost)
|
||||
})
|
||||
m.Group("/actions/general", func() {
|
||||
m.Get("", repo_setting.ActionsGeneralSettings)
|
||||
m.Post("/actions_unit", repo_setting.ActionsUnitPost)
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
@@ -602,6 +603,16 @@ func RepoAssignment(ctx *Context) {
|
||||
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
|
||||
}
|
||||
|
||||
// Check if wiki has content (for non-member visibility)
|
||||
hasWiki, _ := gitrepo.IsRepositoryExist(ctx, repo.WikiStorageRepo())
|
||||
ctx.Data["HasWiki"] = hasWiki
|
||||
|
||||
// Check if there are any packages (for non-member visibility)
|
||||
numPackages, _ := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
RepoID: repo.ID,
|
||||
})
|
||||
ctx.Data["HasPackages"] = numPackages > 0
|
||||
|
||||
if ctx.IsSigned {
|
||||
ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, repo.ID)
|
||||
ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID)
|
||||
|
||||
@@ -26,6 +26,7 @@ type CreateOrgForm struct {
|
||||
OrgName string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"`
|
||||
Visibility structs.VisibleType
|
||||
RepoAdminChangeTeamAccess bool
|
||||
IsHomepagePinned bool
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
@@ -43,6 +44,7 @@ type UpdateOrgSettingForm struct {
|
||||
Location string `binding:"MaxSize(50)"`
|
||||
MaxRepoCreation int
|
||||
RepoAdminChangeTeamAccess bool
|
||||
IsHomepagePinned bool
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
|
||||
@@ -91,6 +91,7 @@ func (f *MigrateRepoForm) Validate(req *http.Request, errs binding.Errors) bindi
|
||||
type RepoSettingForm struct {
|
||||
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
|
||||
Description string `binding:"MaxSize(2048)"`
|
||||
DisplayTitle string `binding:"MaxSize(255)"`
|
||||
Website string `binding:"ValidUrl;MaxSize(1024)"`
|
||||
Interval string
|
||||
MirrorAddress string
|
||||
|
||||
@@ -100,18 +100,21 @@ func GetPagesConfig(ctx context.Context, repo *repo_model.Repository) (*pages_mo
|
||||
|
||||
// getDefaultConfig returns a default landing page configuration
|
||||
func getDefaultConfig(repo *repo_model.Repository, template string) *pages_module.LandingConfig {
|
||||
if template == "" {
|
||||
template = "simple"
|
||||
if template == "" || !pages_module.IsValidTemplate(template) {
|
||||
template = "open-source-hero"
|
||||
}
|
||||
return &pages_module.LandingConfig{
|
||||
Enabled: true,
|
||||
Template: template,
|
||||
Hero: pages_module.HeroConfig{
|
||||
Title: repo.Name,
|
||||
Tagline: repo.Description,
|
||||
Brand: pages_module.BrandConfig{
|
||||
Name: repo.Name,
|
||||
},
|
||||
Branding: pages_module.BrandingConfig{
|
||||
PrimaryColor: "#4183c4",
|
||||
Hero: pages_module.HeroConfig{
|
||||
Headline: repo.Name,
|
||||
Subheadline: repo.Description,
|
||||
},
|
||||
Theme: pages_module.ThemeConfig{
|
||||
Mode: "auto",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -182,7 +185,7 @@ func IsPagesEnabled(ctx context.Context, repo *repo_model.Repository) (bool, err
|
||||
// EnablePages enables pages for a repository with default config
|
||||
func EnablePages(ctx context.Context, repo *repo_model.Repository, template string) error {
|
||||
if !pages_module.IsValidTemplate(template) {
|
||||
template = "simple"
|
||||
template = "open-source-hero"
|
||||
}
|
||||
|
||||
return repo_model.EnablePages(ctx, repo.ID, repo_model.PagesTemplate(template))
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import path from 'node:path';
|
||||
import type {Config} from 'stylelint';
|
||||
|
||||
// Use import.meta.dirname for reliable path resolution in ESM context
|
||||
// This gives us the directory where this config file lives (project root)
|
||||
const cssVarFiles = [
|
||||
fileURLToPath(new URL('web_src/css/base.css', import.meta.url)),
|
||||
fileURLToPath(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url)),
|
||||
fileURLToPath(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url)),
|
||||
path.join(import.meta.dirname, 'web_src/css/base.css'),
|
||||
path.join(import.meta.dirname, 'web_src/css/themes/theme-gitea-light.css'),
|
||||
path.join(import.meta.dirname, 'web_src/css/themes/theme-gitea-dark.css'),
|
||||
];
|
||||
|
||||
export default {
|
||||
@@ -133,9 +135,9 @@ export default {
|
||||
'media-feature-name-no-vendor-prefix': true,
|
||||
'no-descending-specificity': null,
|
||||
'no-invalid-position-at-import-rule': [true, {ignoreAtRules: ['tailwind']}],
|
||||
'no-unknown-animations': null, // disabled until stylelint supports multi-file linting
|
||||
'no-unknown-custom-media': null, // disabled until stylelint supports multi-file linting
|
||||
'no-unknown-custom-properties': null, // disabled until stylelint supports multi-file linting
|
||||
'no-unknown-animations': null,
|
||||
'no-unknown-custom-media': null,
|
||||
'no-unknown-custom-properties': null,
|
||||
'plugin/declaration-block-no-ignored-properties': true,
|
||||
'scale-unlimited/declaration-strict-value': [['/color$/', 'font-weight'], {ignoreValues: '/^(inherit|transparent|unset|initial|currentcolor|none)$/', ignoreFunctions: true, disableFix: true, expandShorthand: true}],
|
||||
'selector-attribute-quotes': 'always',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
|
||||
|
||||
{{template "admin/config_settings/theme" .}}
|
||||
|
||||
{{template "admin/config_settings/avatars" .}}
|
||||
|
||||
{{template "admin/config_settings/repository" .}}
|
||||
|
||||
170
templates/admin/config_settings/theme.tmpl
Normal file
@@ -0,0 +1,170 @@
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.config.theme_config"}}
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<dl class="admin-dl-horizontal">
|
||||
<dt>{{ctx.Locale.Tr "admin.config.disable_registration"}}</dt>
|
||||
<dd>
|
||||
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_registration_desc"}}">
|
||||
<input type="checkbox" data-config-dyn-key="theme.disable_registration" {{if .SystemConfig.Theme.DisableRegistration.Value ctx}}checked{{end}}><label></label>
|
||||
</div>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.hide_explore_users"}}</dt>
|
||||
<dd>
|
||||
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.hide_explore_users_desc"}}">
|
||||
<input type="checkbox" data-config-dyn-key="theme.hide_explore_users" {{if .SystemConfig.Theme.HideExploreUsers.Value ctx}}checked{{end}}><label></label>
|
||||
</div>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.help_url"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.help_url">
|
||||
<div class="field">
|
||||
<input type="text" name="value" value="{{.SystemConfig.Theme.HelpURL.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.help_url_placeholder"}}">
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.help_url_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_site_icon"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/config/theme/icon" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.current_icon"}}</label>
|
||||
<img src="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}" alt="Current Icon" style="max-height: 32px; max-width: 32px; background: var(--color-secondary-bg); padding: 4px; border-radius: 4px;">
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.icon_url"}}</label>
|
||||
<input type="text" name="custom_icon_url" value="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_icon_url_placeholder"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.upload_icon"}}</label>
|
||||
<input type="file" name="icon_file" accept="image/svg+xml,image/png,image/x-icon,image/vnd.microsoft.icon">
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.site_icon_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
|
||||
<button class="ui red button" type="submit" name="action" value="reset">{{ctx.Locale.Tr "admin.config.reset_icon"}}</button>
|
||||
{{end}}
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_home_logo"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/config/theme/logo" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
{{if .SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.current_logo"}}</label>
|
||||
<img src="{{.SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}" alt="Current Logo" style="max-height: 80px; max-width: 200px; background: var(--color-secondary-bg); padding: 8px; border-radius: 4px;">
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.logo_url"}}</label>
|
||||
<input type="text" name="custom_logo_url" value="{{.SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_logo_url_placeholder"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.upload_logo"}}</label>
|
||||
<input type="file" name="logo_file" accept="image/svg+xml,image/png,image/jpeg,image/gif">
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.home_logo_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
{{if .SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}
|
||||
<button class="ui red button" type="submit" name="action" value="reset">{{ctx.Locale.Tr "admin.config.reset_logo"}}</button>
|
||||
{{end}}
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_home_title"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.custom_home_title">
|
||||
<div class="field">
|
||||
<input type="text" name="value" value="{{.SystemConfig.Theme.CustomHomeTitle.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_home_title_placeholder"}}" maxlength="100">
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.custom_home_title_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_home_tagline"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.custom_home_tagline">
|
||||
<div class="field">
|
||||
<input type="text" name="value" value="{{.SystemConfig.Theme.CustomHomeTagline.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_home_tagline_placeholder"}}" maxlength="255">
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.custom_home_tagline_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.pinned_org_display_format"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.pinned_org_display_format">
|
||||
<div class="field">
|
||||
<select class="ui dropdown" name="value">
|
||||
<option value="promotional" {{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "promotional"}}selected{{end}}>{{ctx.Locale.Tr "admin.config.pinned_org_format_promotional"}}</option>
|
||||
<option value="condensed" {{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "condensed"}}selected{{end}}>{{ctx.Locale.Tr "admin.config.pinned_org_format_condensed"}}</option>
|
||||
<option value="regular" {{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "regular"}}selected{{end}}>{{ctx.Locale.Tr "admin.config.pinned_org_format_regular"}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.pinned_org_display_format_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.explore_org_display_format"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.explore_org_display_format">
|
||||
<div class="field">
|
||||
<select class="ui dropdown" name="value">
|
||||
<option value="list" {{if eq (.SystemConfig.Theme.ExploreOrgDisplayFormat.Value ctx) "list"}}selected{{end}}>{{ctx.Locale.Tr "admin.config.explore_org_format_list"}}</option>
|
||||
<option value="tiles" {{if eq (.SystemConfig.Theme.ExploreOrgDisplayFormat.Value ctx) "tiles"}}selected{{end}}>{{ctx.Locale.Tr "admin.config.explore_org_format_tiles"}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.explore_org_display_format_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.api_header_url"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.api_header_url">
|
||||
<div class="field">
|
||||
<input type="text" name="value" value="{{.SystemConfig.Theme.APIHeaderURL.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.api_header_url_placeholder"}}">
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.api_header_url_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_home_html"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.custom_home_html">
|
||||
<div class="field">
|
||||
<textarea id="custom-home-html-editor" name="value" rows="15" placeholder="{{ctx.Locale.Tr "admin.config.custom_home_html_placeholder"}}" style="font-family: var(--fonts-monospace); font-size: 13px;">{{.SystemConfig.Theme.CustomHomeHTML.Value ctx}}</textarea>
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.custom_home_html_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
@@ -1,7 +1,8 @@
|
||||
<footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}">
|
||||
<div class="left-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.software"}}">
|
||||
{{if ShowFooterPoweredBy}}
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://about.gitea.com">{{ctx.Locale.Tr "powered_by" "Gitea"}}</a>
|
||||
<span>{{ctx.Locale.Tr "powered_by" "GitCaddy Server"}}</span>
|
||||
<span class="tw-text-secondary tw-text-12">{{ctx.Locale.Tr "based_on"}} <a target="_blank" rel="noopener noreferrer" href="https://about.gitea.com">Gitea</a></span>
|
||||
{{end}}
|
||||
{{if (or .ShowFooterVersion .PageIsAdmin)}}
|
||||
{{ctx.Locale.Tr "version"}}:
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
<link rel="alternate" type="application/atom+xml" title="" href="{{.FeedURL}}.atom">
|
||||
<link rel="alternate" type="application/rss+xml" title="" href="{{.FeedURL}}.rss">
|
||||
{{end}}
|
||||
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
|
||||
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
|
||||
<link rel="icon" href="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}">
|
||||
{{else}}
|
||||
<link rel="icon" href="{{AssetUrlPrefix}}/img/gitcaddy-icon.svg" type="image/svg+xml">
|
||||
<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png">
|
||||
{{end}}
|
||||
{{template "base/head_opengraph" .}}
|
||||
{{template "base/head_style" .}}
|
||||
{{template "base/head_script" .}}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
<div class="navbar-left">
|
||||
<!-- the logo -->
|
||||
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
|
||||
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
|
||||
<img width="30" height="30" src="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
{{else}}
|
||||
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
{{end}}
|
||||
</a>
|
||||
|
||||
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
|
||||
@@ -27,16 +31,22 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
<a class="item{{if .PageIsExplore}} active{{end}}" href="{{AppSubUrl}}/explore/repos">{{ctx.Locale.Tr "explore_title"}}</a>
|
||||
{{if .SystemConfig.Theme.APIHeaderURL.Value ctx}}
|
||||
<a class="item" href="{{.SystemConfig.Theme.APIHeaderURL.Value ctx}}">{{ctx.Locale.Tr "api"}}</a>
|
||||
{{end}}
|
||||
{{else if .IsLandingPageOrganizations}}
|
||||
<a class="item{{if .PageIsExplore}} active{{end}}" href="{{AppSubUrl}}/explore/organizations">{{ctx.Locale.Tr "explore_title"}}</a>
|
||||
{{else}}
|
||||
<a class="item{{if .PageIsExplore}} active{{end}}" href="{{AppSubUrl}}/explore/repos">{{ctx.Locale.Tr "explore_title"}}</a>
|
||||
{{end}}
|
||||
{{if .SystemConfig.Theme.APIHeaderURL.Value ctx}}
|
||||
<a class="item" href="{{.SystemConfig.Theme.APIHeaderURL.Value ctx}}">{{ctx.Locale.Tr "api"}}</a>
|
||||
{{end}}
|
||||
|
||||
{{template "custom/extra_links" .}}
|
||||
|
||||
{{if not .IsSigned}}
|
||||
<a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com">{{ctx.Locale.Tr "help"}}</a>
|
||||
{{if .SystemConfig.Theme.HelpURL.Value ctx}}<a class="item" target="_blank" rel="noopener noreferrer" href="{{.SystemConfig.Theme.HelpURL.Value ctx}}">{{ctx.Locale.Tr "help"}}</a>{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -116,10 +126,10 @@
|
||||
{{svg "octicon-tools"}}
|
||||
{{ctx.Locale.Tr "your_settings"}}
|
||||
</a>
|
||||
<a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com">
|
||||
{{if .SystemConfig.Theme.HelpURL.Value ctx}}<a class="item" target="_blank" rel="noopener noreferrer" href="{{.SystemConfig.Theme.HelpURL.Value ctx}}">
|
||||
{{svg "octicon-question"}}
|
||||
{{ctx.Locale.Tr "help"}}
|
||||
</a>
|
||||
</a>{{end}}
|
||||
{{if .IsAdmin}}
|
||||
<div class="divider"></div>
|
||||
<a class="{{if .PageIsAdmin}}active {{end}}item" href="{{AppSubUrl}}/-/admin">
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
{{if and .PageIsExploreOrganizations (eq (.SystemConfig.Theme.ExploreOrgDisplayFormat.Value ctx) "tiles")}}
|
||||
{{/* Tile Cards View for Organizations */}}
|
||||
<div class="ui four doubling stackable cards">
|
||||
{{range .Users}}
|
||||
<a class="ui card" href="{{.HomeLink}}">
|
||||
<div class="content tw-text-center">
|
||||
<div class="tw-mb-3">
|
||||
{{ctx.AvatarUtils.Avatar . 64 "tw-rounded"}}
|
||||
</div>
|
||||
<div class="header">{{.DisplayName}}</div>
|
||||
{{if .Description}}
|
||||
<div class="meta tw-mt-2">{{.Description}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</a>
|
||||
{{else}}
|
||||
<div class="ui card">
|
||||
<div class="content">
|
||||
{{ctx.Locale.Tr "search.no_results"}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{/* Default List View */}}
|
||||
<div class="flex-list">
|
||||
{{range .Users}}
|
||||
<div class="flex-item tw-items-center">
|
||||
@@ -6,7 +31,7 @@
|
||||
</div>
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">
|
||||
{{template "shared/user/name" .}}
|
||||
{{if $.PageIsExploreOrganizations}}<a class="text muted" href="{{.HomeLink}}">{{.DisplayName}}</a>{{else}}<a class="text muted" href="{{.HomeLink}}">{{.Name}}</a>{{end}}
|
||||
{{if .Visibility.IsPrivate}}
|
||||
<span class="ui basic tiny label">{{ctx.Locale.Tr "repo.desc.private"}}</span>
|
||||
{{end}}
|
||||
@@ -31,3 +56,4 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -1,31 +1,52 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}" class="page-content home">
|
||||
{{if .SystemConfig.Theme.CustomHomeHTML.Value ctx}}
|
||||
{{/* Custom homepage content */}}
|
||||
{{.SystemConfig.Theme.CustomHomeHTML.Value ctx | SafeHTML}}
|
||||
{{else}}
|
||||
{{/* Default homepage content */}}
|
||||
<div class="tw-mb-8 tw-px-8">
|
||||
<div class="center">
|
||||
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
{{if .SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}
|
||||
<img class="logo" width="220" height="220" src="{{.SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
{{else}}
|
||||
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/gitcaddy-logo.svg" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
{{end}}
|
||||
<div class="hero">
|
||||
<h1 class="ui icon header title tw-text-balance">
|
||||
{{AppName}}
|
||||
{{if .SystemConfig.Theme.CustomHomeTitle.Value ctx}}
|
||||
{{.SystemConfig.Theme.CustomHomeTitle.Value ctx}}
|
||||
{{else}}
|
||||
{{AppName}}
|
||||
{{end}}
|
||||
</h1>
|
||||
<h2 class="tw-text-balance">{{ctx.Locale.Tr "startpage.app_desc"}}</h2>
|
||||
<h2 class="tw-text-balance">
|
||||
{{if .SystemConfig.Theme.CustomHomeTagline.Value ctx}}
|
||||
{{.SystemConfig.Theme.CustomHomeTagline.Value ctx}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "startpage.app_desc"}}
|
||||
{{end}}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "promotional"}}
|
||||
{{/* Promotional format: show marketing content */}}
|
||||
<div class="ui stackable middle very relaxed page grid">
|
||||
<div class="eight wide center column">
|
||||
<h1 class="hero ui icon header">
|
||||
{{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}}
|
||||
</h1>
|
||||
<p class="large tw-text-balance">
|
||||
{{ctx.Locale.Tr "startpage.install_desc" "https://docs.gitea.com/installation/install-from-binary" "https://github.com/go-gitea/gitea/tree/master/docker" "https://docs.gitea.com/installation/install-from-package"}}
|
||||
{{ctx.Locale.Tr "startpage.install_desc"}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="eight wide center column">
|
||||
<h1 class="hero ui icon header">
|
||||
{{svg "octicon-device-desktop"}} {{ctx.Locale.Tr "startpage.platform"}}
|
||||
{{svg "octicon-dependabot"}} {{ctx.Locale.Tr "startpage.platform"}}
|
||||
</h1>
|
||||
<p class="large tw-text-balance">
|
||||
{{ctx.Locale.Tr "startpage.platform_desc" "https://go.dev/"}}
|
||||
{{ctx.Locale.Tr "startpage.platform_desc"}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,9 +64,90 @@
|
||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}
|
||||
</h1>
|
||||
<p class="large tw-text-balance">
|
||||
{{ctx.Locale.Tr "startpage.license_desc" "https://code.gitea.io/gitea" "code.gitea.io/gitea" "https://github.com/go-gitea/gitea"}}
|
||||
{{ctx.Locale.Tr "startpage.license_desc"}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{else if .PinnedOrganizations}}
|
||||
{{/* Show pinned organizations */}}
|
||||
<div class="ui container tw-my-8">
|
||||
{{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "regular"}}
|
||||
{{/* Regular format: icon above, title below, description below that */}}
|
||||
<div class="ui four doubling stackable cards">
|
||||
{{range .PinnedOrganizations}}
|
||||
<a class="ui card" href="{{.HomeLink}}">
|
||||
<div class="content tw-text-center">
|
||||
<div class="tw-mb-3">
|
||||
{{ctx.AvatarUtils.Avatar . 64 "tw-rounded"}}
|
||||
</div>
|
||||
<div class="header">{{.DisplayName}}</div>
|
||||
{{if .Description}}
|
||||
<div class="meta tw-mt-2">{{.Description}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{/* Condensed format (default): icon on left, title/description on right */}}
|
||||
<div class="ui four doubling stackable cards">
|
||||
{{range .PinnedOrganizations}}
|
||||
<a class="ui card" href="{{.HomeLink}}">
|
||||
<div class="content">
|
||||
<div class="tw-flex tw-items-center tw-gap-3">
|
||||
{{ctx.AvatarUtils.Avatar . 48 "tw-rounded"}}
|
||||
<div class="tw-flex-1 tw-overflow-hidden">
|
||||
<div class="header tw-truncate">{{.DisplayName}}</div>
|
||||
{{if .Description}}
|
||||
<div class="meta tw-truncate">{{.Description}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{/* No pinned organizations - show promotional content as fallback */}}
|
||||
<div class="ui stackable middle very relaxed page grid">
|
||||
<div class="eight wide center column">
|
||||
<h1 class="hero ui icon header">
|
||||
{{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}}
|
||||
</h1>
|
||||
<p class="large tw-text-balance">
|
||||
{{ctx.Locale.Tr "startpage.install_desc"}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="eight wide center column">
|
||||
<h1 class="hero ui icon header">
|
||||
{{svg "octicon-dependabot"}} {{ctx.Locale.Tr "startpage.platform"}}
|
||||
</h1>
|
||||
<p class="large tw-text-balance">
|
||||
{{ctx.Locale.Tr "startpage.platform_desc"}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui stackable middle very relaxed page grid">
|
||||
<div class="eight wide center column">
|
||||
<h1 class="hero ui icon header">
|
||||
{{svg "octicon-rocket"}} {{ctx.Locale.Tr "startpage.lightweight"}}
|
||||
</h1>
|
||||
<p class="large tw-text-balance">
|
||||
{{ctx.Locale.Tr "startpage.lightweight_desc"}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="eight wide center column">
|
||||
<h1 class="hero ui icon header">
|
||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}
|
||||
</h1>
|
||||
<p class="large tw-text-balance">
|
||||
{{ctx.Locale.Tr "startpage.license_desc"}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
{{/* Overview Tab Content */}}
|
||||
{{if .PageIsViewOverview}}
|
||||
{{/* Pinned Repositories Section */}}
|
||||
{{if or .IsOrganizationMember .HasPublicPinnedRepos}}{{/* Pinned Repositories Section */}}
|
||||
<div class="ui segment pinned-repos-section">
|
||||
<h4 class="ui header tw-flex tw-items-center">
|
||||
{{svg "octicon-pin" 16}} {{ctx.Locale.Tr "org.pinned_repos"}}
|
||||
@@ -28,7 +28,7 @@
|
||||
{{if .UngroupedPinned}}
|
||||
<div class="ui three stackable cards pinned-repos">
|
||||
{{range .UngroupedPinned}}
|
||||
{{if .Repo}}
|
||||
{{if and .Repo (or $.IsOrganizationMember (not .Repo.IsPrivate))}}
|
||||
<a class="ui card" href="{{.Repo.Link}}">
|
||||
<div class="content tw-text-center">
|
||||
{{if .Repo.Avatar}}
|
||||
@@ -38,7 +38,7 @@
|
||||
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 48}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 48}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 48}}{{else}}{{svg "octicon-repo" 48}}{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="header tw-mt-2">{{.Repo.Name}}</div>
|
||||
<div class="header tw-mt-2">{{if .Repo.DisplayTitle}}{{.Repo.DisplayTitle}}{{else}}{{.Repo.Name}}{{end}}</div>
|
||||
{{if .Repo.Description}}
|
||||
<div class="description text grey tw-text-sm tw-mt-1">{{.Repo.Description}}</div>
|
||||
{{end}}
|
||||
@@ -73,7 +73,7 @@
|
||||
</h5>
|
||||
<div class="ui three stackable cards pinned-repos">
|
||||
{{range $groupRepos}}
|
||||
{{if .Repo}}
|
||||
{{if and .Repo (or $.IsOrganizationMember (not .Repo.IsPrivate))}}
|
||||
<a class="ui card" href="{{.Repo.Link}}">
|
||||
<div class="content tw-text-center">
|
||||
{{if .Repo.Avatar}}
|
||||
@@ -83,7 +83,7 @@
|
||||
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 48}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 48}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 48}}{{else}}{{svg "octicon-repo" 48}}{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="header tw-mt-2">{{.Repo.Name}}</div>
|
||||
<div class="header tw-mt-2">{{if .Repo.DisplayTitle}}{{.Repo.DisplayTitle}}{{else}}{{.Repo.Name}}{{end}}</div>
|
||||
{{if .Repo.Description}}
|
||||
<div class="description text grey tw-text-sm tw-mt-1">{{.Repo.Description}}</div>
|
||||
{{end}}
|
||||
@@ -110,7 +110,7 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{/* Empty state for pinned repos */}}
|
||||
{{if .IsOrganizationMember}}{{/* Empty state for pinned repos - members only */}}
|
||||
<div class="ui placeholder segment tw-text-center">
|
||||
<div class="ui icon header">
|
||||
{{svg "octicon-pin" 48}}
|
||||
@@ -129,8 +129,10 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* Profile README Empty State */}}
|
||||
{{if and (not .ProfileReadmeContent) .IsOrganizationOwner}}
|
||||
@@ -172,7 +174,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="tw-flex-1 tw-min-w-0">
|
||||
<a href="{{.Repo.Link}}" class="tw-font-semibold">{{.Repo.Name}}</a>
|
||||
<a href="{{.Repo.Link}}" class="tw-font-semibold">{{if .Repo.DisplayTitle}}{{.Repo.DisplayTitle}}{{else}}{{.Repo.Name}}{{end}}</a>
|
||||
{{if .CommitMessage}}
|
||||
<p class="text grey tw-text-sm tw-truncate tw-mb-0">{{.CommitMessage}}</p>
|
||||
{{end}}
|
||||
@@ -305,14 +307,7 @@
|
||||
<a href="{{.User.HomeLink}}" title="{{.User.Name}}{{if .User.FullName}} ({{.User.FullName}}){{end}}">{{ctx.AvatarUtils.Avatar .User 48}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<h4 class="ui top attached header tw-flex tw-mt-4">
|
||||
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.public_members"}}</strong>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p class="text grey tw-text-center">{{ctx.Locale.Tr "org.no_public_members"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="ui small label">{{.RepoCount}}</div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{if .CanReadProjects}}
|
||||
{{if and .CanReadProjects (or .IsOrganizationMember .ProjectCount)}}
|
||||
<a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects">
|
||||
{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
|
||||
{{if .ProjectCount}}
|
||||
@@ -18,7 +18,7 @@
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if and .IsPackageEnabled .CanReadPackages}}
|
||||
{{if and .IsPackageEnabled .CanReadPackages .IsOrganizationMember}}
|
||||
<a class="{{if .IsPackagesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/packages">
|
||||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
|
||||
</a>
|
||||
|
||||
@@ -46,6 +46,17 @@
|
||||
<input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}">
|
||||
<p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.homepage_pinning"}}</label>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="is_homepage_pinned" {{if .IsHomepagePinned}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "org.settings.pin_to_homepage"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.pin_to_homepage_help"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="field">
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{if .Config.Hero.Title}}{{.Config.Hero.Title}}{{else}}{{.Repository.Name}}{{end}} - {{.Repository.Owner.Name}}</title>
|
||||
<meta name="description" content="{{if .Config.Hero.Tagline}}{{.Config.Hero.Tagline}}{{else}}{{.Repository.Description}}{{end}}">
|
||||
<title>{{if .Config.Hero.Headline}}{{.Config.Hero.Headline}}{{else}}{{.Repository.Name}}{{end}} - {{.Repository.Owner.Name}}</title>
|
||||
<meta name="description" content="{{if .Config.Hero.Subheadline}}{{.Config.Hero.Subheadline}}{{else}}{{.Repository.Description}}{{end}}">
|
||||
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
|
||||
{{template "base/head_style" .}}
|
||||
<style>
|
||||
/* Pages standalone styles - no Gitea navbar */
|
||||
:root {
|
||||
--pages-primary: {{if .Config.Branding.PrimaryColor}}{{.Config.Branding.PrimaryColor}}{{else}}#4183c4{{end}};
|
||||
--pages-secondary: {{if .Config.Branding.SecondaryColor}}{{.Config.Branding.SecondaryColor}}{{else}}#6c757d{{end}};
|
||||
--pages-primary: {{if .Config.Theme.PrimaryColor}}{{.Config.Theme.PrimaryColor}}{{else}}#4183c4{{end}};
|
||||
--pages-secondary: {{if .Config.Theme.AccentColor}}{{.Config.Theme.AccentColor}}{{else}}#6c757d{{end}};
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body.pages-body {
|
||||
|
||||
1000
templates/pages/bold-marketing.tmpl
Normal file
@@ -1,164 +0,0 @@
|
||||
{{template "pages/base_head" .}}
|
||||
<style>
|
||||
/* Documentation-specific styles */
|
||||
.pages-documentation {
|
||||
background: #fff;
|
||||
}
|
||||
.pages-documentation .pages-header {
|
||||
background: #24292e;
|
||||
border-bottom: none;
|
||||
}
|
||||
.pages-documentation .pages-nav-brand,
|
||||
.pages-documentation .pages-nav-link {
|
||||
color: #fff;
|
||||
}
|
||||
.pages-documentation .pages-nav-link:hover {
|
||||
color: #79b8ff;
|
||||
}
|
||||
.pages-docs-layout {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 65px);
|
||||
}
|
||||
.pages-docs-sidebar {
|
||||
width: 280px;
|
||||
background: #f6f8fa;
|
||||
border-right: 1px solid #e1e4e8;
|
||||
padding: 24px;
|
||||
position: sticky;
|
||||
top: 65px;
|
||||
height: calc(100vh - 65px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.pages-docs-search {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.pages-search-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.pages-docs-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.pages-docs-section-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #586069;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.pages-docs-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.pages-docs-list li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pages-docs-list a {
|
||||
color: #24292e;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
display: block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.pages-docs-list a:hover {
|
||||
background: #e1e4e8;
|
||||
color: var(--pages-primary);
|
||||
}
|
||||
.pages-docs-content {
|
||||
flex: 1;
|
||||
padding: 48px;
|
||||
max-width: 900px;
|
||||
}
|
||||
.pages-docs-article {
|
||||
line-height: 1.7;
|
||||
}
|
||||
.pages-docs-article h1,
|
||||
.pages-docs-article h2,
|
||||
.pages-docs-article h3 {
|
||||
margin-top: 32px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
}
|
||||
.pages-docs-article code {
|
||||
background: #f6f8fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.pages-docs-article pre {
|
||||
background: #24292e;
|
||||
color: #e1e4e8;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.pages-docs-article pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.pages-docs-sidebar {
|
||||
display: none;
|
||||
}
|
||||
.pages-docs-content {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="pages-landing pages-documentation">
|
||||
{{template "pages/header" .}}
|
||||
|
||||
<div class="pages-docs-layout">
|
||||
<aside class="pages-docs-sidebar">
|
||||
<div class="pages-docs-search">
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "search"}}..." class="pages-search-input">
|
||||
</div>
|
||||
<nav class="pages-docs-nav">
|
||||
{{if .Config.Documentation.Sidebar}}
|
||||
{{range .Config.Documentation.Sidebar}}
|
||||
<div class="pages-docs-section {{if .Collapsed}}collapsed{{end}}">
|
||||
{{if .Title}}
|
||||
<h4 class="pages-docs-section-title">{{.Title}}</h4>
|
||||
{{end}}
|
||||
<ul class="pages-docs-list">
|
||||
{{range .Items}}
|
||||
<li><a href="/docs/{{.}}">{{.}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="pages-docs-section">
|
||||
<h4 class="pages-docs-section-title">Documentation</h4>
|
||||
<ul class="pages-docs-list">
|
||||
<li><a href="#">Getting Started</a></li>
|
||||
<li><a href="#">Installation</a></li>
|
||||
<li><a href="#">Configuration</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="pages-docs-content">
|
||||
<article class="pages-docs-article">
|
||||
{{if .ReadmeContent}}
|
||||
<div class="markup">
|
||||
{{.ReadmeContent}}
|
||||
</div>
|
||||
{{else}}
|
||||
<h1>{{.Repository.Name}}</h1>
|
||||
<p>{{if .Repository.Description}}{{.Repository.Description}}{{else}}Welcome to the documentation.{{end}}</p>
|
||||
{{end}}
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
{{template "pages/base_footer" .}}
|
||||
@@ -17,7 +17,7 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
<a href="{{AppSubUrl}}/{{.Repository.FullName}}" class="ui mini button" target="_blank">
|
||||
{{svg "octicon-mark-github" 16}} View Source
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy"> View Source
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
811
templates/pages/minimalist-docs.tmpl
Normal file
@@ -0,0 +1,811 @@
|
||||
{{template "pages/base_head" .}}
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;500;600;700&family=Source+Code+Pro:wght@400;500&display=swap');
|
||||
|
||||
:root {
|
||||
--md-bg: #fcfcfc;
|
||||
--md-text: #171717;
|
||||
--md-muted: #525252;
|
||||
--md-light: #737373;
|
||||
--md-border: #e5e5e5;
|
||||
--md-accent: {{if .Config.Theme.PrimaryColor}}{{.Config.Theme.PrimaryColor}}{{else}}#171717{{end}};
|
||||
}
|
||||
|
||||
.md-page {
|
||||
min-height: 100vh;
|
||||
background: var(--md-bg);
|
||||
color: var(--md-text);
|
||||
font-family: 'Source Serif 4', Georgia, serif;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.md-nav {
|
||||
padding: 24px 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--md-border);
|
||||
}
|
||||
|
||||
.md-nav-brand {
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
color: var(--md-text);
|
||||
letter-spacing: -0.02em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.md-nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.md-nav-link {
|
||||
color: var(--md-muted);
|
||||
text-decoration: none;
|
||||
font-size: 15px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.md-nav-link:hover {
|
||||
color: var(--md-text);
|
||||
}
|
||||
|
||||
.md-btn-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--md-muted);
|
||||
font-family: 'Source Serif 4', Georgia, serif;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.md-btn-text:hover {
|
||||
color: var(--md-text);
|
||||
}
|
||||
|
||||
.md-btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 28px;
|
||||
background: var(--md-text);
|
||||
color: var(--md-bg);
|
||||
font-family: 'Source Serif 4', Georgia, serif;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.md-btn-primary:hover {
|
||||
background: #404040;
|
||||
}
|
||||
|
||||
.md-hero {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 120px 24px 80px;
|
||||
}
|
||||
|
||||
.md-hero h1 {
|
||||
font-size: 52px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 24px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--md-text);
|
||||
}
|
||||
|
||||
.md-hero-sub {
|
||||
font-size: 21px;
|
||||
color: var(--md-muted);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.md-hero-ctas {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
|
||||
.md-install-cmd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
font-size: 15px;
|
||||
color: var(--md-muted);
|
||||
border: 1px solid var(--md-border);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.md-install-cmd:hover {
|
||||
border-color: #d4d4d4;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.md-code-block {
|
||||
background: #1a1a1a;
|
||||
border-radius: 6px;
|
||||
padding: 28px;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.md-code-block code {
|
||||
color: #e5e5e5;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.md-copy-btn {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 6px;
|
||||
color: #737373;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.md-copy-btn:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #e5e5e5;
|
||||
}
|
||||
|
||||
.md-divider {
|
||||
height: 1px;
|
||||
background: var(--md-border);
|
||||
max-width: 720px;
|
||||
margin: 80px auto;
|
||||
}
|
||||
|
||||
.md-stats {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.md-stats-inner {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.md-stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: var(--md-text);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.md-stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--md-light);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.md-section {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px 80px;
|
||||
}
|
||||
|
||||
.md-section-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 32px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.md-value-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.md-value-bullet {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--md-text);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.md-value-title {
|
||||
font-weight: 500;
|
||||
color: var(--md-text);
|
||||
}
|
||||
|
||||
.md-value-desc {
|
||||
color: var(--md-light);
|
||||
}
|
||||
|
||||
.md-accordion {
|
||||
border-top: 1px solid var(--md-border);
|
||||
}
|
||||
|
||||
.md-accordion-item {
|
||||
border-bottom: 1px solid var(--md-border);
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.md-accordion-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-family: 'Source Serif 4', Georgia, serif;
|
||||
}
|
||||
|
||||
.md-accordion-title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--md-text);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.md-accordion-header:hover .md-accordion-title {
|
||||
color: var(--md-muted);
|
||||
}
|
||||
|
||||
.md-accordion-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--md-light);
|
||||
transition: transform 0.3s ease;
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.md-accordion-content {
|
||||
display: none;
|
||||
padding-top: 16px;
|
||||
font-size: 17px;
|
||||
line-height: 1.7;
|
||||
color: var(--md-muted);
|
||||
}
|
||||
|
||||
.md-accordion-item.open .md-accordion-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.md-pricing {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 80px 24px;
|
||||
border-top: 1px solid var(--md-border);
|
||||
}
|
||||
|
||||
.md-pricing-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.md-pricing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.md-pricing-card {
|
||||
border: 1px solid var(--md-border);
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
background: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.md-pricing-card.featured {
|
||||
border-color: var(--md-text);
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.md-pricing-featured-badge {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--md-text);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.md-pricing-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.md-pricing-price {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.md-pricing-period {
|
||||
font-size: 14px;
|
||||
color: var(--md-light);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.md-pricing-features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.md-pricing-features li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
font-size: 15px;
|
||||
color: var(--md-muted);
|
||||
}
|
||||
|
||||
.md-pricing-features li::before {
|
||||
content: "✓";
|
||||
color: var(--md-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.md-pricing-cta {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--md-border);
|
||||
border-radius: 4px;
|
||||
font-family: 'Source Serif 4', Georgia, serif;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
color: var(--md-text);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.md-pricing-cta:hover {
|
||||
background: var(--md-text);
|
||||
color: white;
|
||||
border-color: var(--md-text);
|
||||
}
|
||||
|
||||
.md-pricing-card.featured .md-pricing-cta {
|
||||
background: var(--md-text);
|
||||
color: white;
|
||||
border-color: var(--md-text);
|
||||
}
|
||||
|
||||
.md-pricing-card.featured .md-pricing-cta:hover {
|
||||
background: #404040;
|
||||
}
|
||||
|
||||
.md-testimonial-section {
|
||||
border-top: 1px solid var(--md-border);
|
||||
border-bottom: 1px solid var(--md-border);
|
||||
padding: 80px 0;
|
||||
margin: 80px 0;
|
||||
}
|
||||
|
||||
.md-testimonial-inner {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.md-testimonial-quote {
|
||||
font-size: 28px;
|
||||
line-height: 1.5;
|
||||
color: var(--md-text);
|
||||
margin-bottom: 24px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.md-testimonial-author {
|
||||
font-weight: 500;
|
||||
color: var(--md-muted);
|
||||
}
|
||||
|
||||
.md-testimonial-role {
|
||||
color: var(--md-light);
|
||||
}
|
||||
|
||||
.md-used-by {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px 80px;
|
||||
}
|
||||
|
||||
.md-used-by-label {
|
||||
font-size: 14px;
|
||||
color: #a3a3a3;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.md-used-by-logos {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.md-used-by-item {
|
||||
font-size: 15px;
|
||||
color: #a3a3a3;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.md-cta-section {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 80px 24px;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--md-border);
|
||||
}
|
||||
|
||||
.md-cta-section h2 {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.md-footer {
|
||||
padding: 32px 48px;
|
||||
border-top: 1px solid var(--md-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.md-footer-copyright {
|
||||
font-size: 14px;
|
||||
color: #a3a3a3;
|
||||
}
|
||||
|
||||
.md-footer-links {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.md-footer-link {
|
||||
color: var(--md-light);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.md-footer-link:hover {
|
||||
color: var(--md-text);
|
||||
}
|
||||
|
||||
.md-footer-social {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.md-footer-social a {
|
||||
color: var(--md-light);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.md-footer-social a:hover {
|
||||
color: var(--md-text);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.md-nav { padding: 16px 24px; }
|
||||
.md-nav-links { gap: 16px; }
|
||||
.md-hero { padding: 80px 24px 60px; }
|
||||
.md-hero h1 { font-size: 36px; }
|
||||
.md-hero-sub { font-size: 18px; }
|
||||
.md-hero-ctas { flex-direction: column; align-items: flex-start; }
|
||||
.md-stats-inner { grid-template-columns: 1fr; gap: 24px; }
|
||||
.md-section-title { font-size: 28px; }
|
||||
.md-testimonial-quote { font-size: 22px; }
|
||||
.md-cta-section h2 { font-size: 28px; }
|
||||
.md-footer { flex-direction: column; gap: 16px; text-align: center; padding: 24px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="md-page">
|
||||
<!-- Navigation -->
|
||||
<nav class="md-nav">
|
||||
<a href="/" class="md-nav-brand">{{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}</a>
|
||||
<div class="md-nav-links">
|
||||
{{if .Config.Footer.Links}}
|
||||
{{range .Config.Footer.Links}}
|
||||
<a href="{{.URL}}" class="md-nav-link">{{.Label}}</a>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<a href="{{.Repository.Link}}/wiki" class="md-nav-link">Docs</a>
|
||||
<a href="{{.Repository.Link}}" class="md-nav-link">API</a>
|
||||
{{end}}
|
||||
{{if .Config.ValueProps}}<a href="#why" class="md-nav-link">Why</a>{{end}}
|
||||
{{if .Config.Features}}<a href="#features" class="md-nav-link">Features</a>{{end}}
|
||||
{{if .Config.Pricing.Plans}}<a href="#pricing" class="md-nav-link">Pricing</a>{{end}}
|
||||
<a href="{{.Repository.Link}}" class="md-btn-text">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="18" height="18" alt="GitCaddy">
|
||||
Repository
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section (Left Aligned) -->
|
||||
<section class="md-hero">
|
||||
<h1>{{if .Config.Hero.Headline}}{{.Config.Hero.Headline}}{{else}}{{.Repository.Name}}{{end}}</h1>
|
||||
<p class="md-hero-sub">{{if .Config.Hero.Subheadline}}{{.Config.Hero.Subheadline}}{{else}}{{.Repository.Description}}{{end}}</p>
|
||||
|
||||
<div class="md-hero-ctas">
|
||||
{{if .Config.Hero.PrimaryCTA.Label}}
|
||||
<a href="{{.Config.Hero.PrimaryCTA.URL}}" class="md-btn-primary">
|
||||
{{.Config.Hero.PrimaryCTA.Label}}
|
||||
{{svg "octicon-arrow-right" 16}}
|
||||
</a>
|
||||
{{else}}
|
||||
<a href="{{.Repository.Link}}" class="md-btn-primary">
|
||||
Get Started
|
||||
{{svg "octicon-arrow-right" 16}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Config.Hero.CodeExample}}
|
||||
<div class="md-code-block">
|
||||
<button class="md-copy-btn" onclick="navigator.clipboard.writeText(document.getElementById('code-example').textContent)">
|
||||
{{svg "octicon-copy" 16}}
|
||||
</button>
|
||||
<code id="code-example">{{.Config.Hero.CodeExample}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
{{if or .Config.Stats (gt .NumStars 0)}}
|
||||
<div class="md-divider"></div>
|
||||
<!-- Stats -->
|
||||
<section class="md-stats">
|
||||
<div class="md-stats-inner">
|
||||
{{if .Config.Stats}}
|
||||
{{range .Config.Stats}}
|
||||
<div>
|
||||
<div class="md-stat-value">{{.Value}}</div>
|
||||
<div class="md-stat-label">{{.Label}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div>
|
||||
<div class="md-stat-value">{{.NumStars}}</div>
|
||||
<div class="md-stat-label">Stars</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="md-stat-value">{{.NumForks}}</div>
|
||||
<div class="md-stat-label">Forks</div>
|
||||
</div>
|
||||
{{if .LatestRelease}}
|
||||
<div>
|
||||
<div class="md-stat-value">v{{.LatestRelease.TagName}}</div>
|
||||
<div class="md-stat-label">Latest</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
<div class="md-divider"></div>
|
||||
{{end}}
|
||||
|
||||
<!-- Value Props (Bullet Points) -->
|
||||
{{if .Config.ValueProps}}
|
||||
<section class="md-section" id="why">
|
||||
<h2 class="md-section-title">{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}{{else}}Why Choose Us{{end}}</h2>
|
||||
{{range .Config.ValueProps}}
|
||||
<div class="md-value-item">
|
||||
<div class="md-value-bullet"></div>
|
||||
<div>
|
||||
<span class="md-value-title">{{.Title}}</span>
|
||||
<span class="md-value-desc"> — {{.Description}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Features Accordion (+/-) -->
|
||||
{{if .Config.Features}}
|
||||
<section class="md-section" id="features">
|
||||
<h2 class="md-section-title">Features</h2>
|
||||
<div class="md-accordion">
|
||||
{{range $i, $f := .Config.Features}}
|
||||
<div class="md-accordion-item" data-index="{{$i}}">
|
||||
<button class="md-accordion-header" onclick="toggleAccordion(this)">
|
||||
<span class="md-accordion-title">{{$f.Title}}</span>
|
||||
<span class="md-accordion-icon">+</span>
|
||||
</button>
|
||||
<div class="md-accordion-content">
|
||||
{{$f.Description}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Pricing Section -->
|
||||
{{if .Config.Pricing.Plans}}
|
||||
<section class="md-pricing" id="pricing">
|
||||
<h2 class="md-pricing-title">{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Pricing{{end}}</h2>
|
||||
<div class="md-pricing-grid">
|
||||
{{range .Config.Pricing.Plans}}
|
||||
<div class="md-pricing-card{{if .Featured}} featured{{end}}">
|
||||
{{if .Featured}}<span class="md-pricing-featured-badge">Popular</span>{{end}}
|
||||
<div class="md-pricing-name">{{.Name}}</div>
|
||||
<div class="md-pricing-price">{{.Price}}</div>
|
||||
<div class="md-pricing-period">{{.Period}}</div>
|
||||
{{if .Features}}
|
||||
<ul class="md-pricing-features">
|
||||
{{range .Features}}
|
||||
<li>{{.}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
<a href="#" class="md-pricing-cta">{{if .CTA}}{{.CTA}}{{else}}Get Started{{end}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Testimonial -->
|
||||
{{if .Config.SocialProof.Testimonials}}
|
||||
<section class="md-testimonial-section">
|
||||
<div class="md-testimonial-inner">
|
||||
<div class="md-testimonials-container">
|
||||
{{range .Config.SocialProof.Testimonials}}
|
||||
<div class="md-testimonial-item" style="display: none;">
|
||||
<blockquote class="md-testimonial-quote">"{{.Quote}}"</blockquote>
|
||||
<div>
|
||||
<span class="md-testimonial-author">{{.Author}}</span>
|
||||
<span class="md-testimonial-role"> · {{.Role}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function() {
|
||||
var items = document.querySelectorAll(".md-testimonial-item");
|
||||
if (items.length > 0) {
|
||||
var idx = Math.floor(Math.random() * items.length);
|
||||
items[idx].style.display = "block";
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<!-- Used By -->
|
||||
{{if .Config.SocialProof.Logos}}
|
||||
<section class="md-used-by">
|
||||
<p class="md-used-by-label">Used by</p>
|
||||
<div class="md-used-by-logos">
|
||||
{{range .Config.SocialProof.Logos}}
|
||||
<span class="md-used-by-item">{{.}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- CTA Section -->
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<section class="md-cta-section">
|
||||
<h2>{{.Config.CTASection.Headline}}</h2>
|
||||
{{if .Config.CTASection.Subheadline}}
|
||||
<button class="md-install-cmd" style="margin-bottom: 32px; font-size: 18px; padding: 16px 28px;" onclick="navigator.clipboard.writeText('{{.Config.CTASection.Subheadline}}')">
|
||||
<span style="color: #a3a3a3;">$</span>
|
||||
{{.Config.CTASection.Subheadline}}
|
||||
{{svg "octicon-copy" 14}}
|
||||
</button>
|
||||
{{end}}
|
||||
{{if .Config.CTASection.Button.Label}}
|
||||
<div>
|
||||
<a href="{{.Config.CTASection.Button.URL}}" class="md-btn-primary">
|
||||
{{.Config.CTASection.Button.Label}}
|
||||
{{svg "octicon-arrow-right" 16}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="md-footer">
|
||||
<span class="md-footer-copyright">{{if .Config.Footer.Copyright}}{{.Config.Footer.Copyright}}{{else}}© <script>document.write(new Date().getFullYear())</script> {{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}{{end}}</span>
|
||||
{{if .Config.Footer.Social}}
|
||||
<div class="md-footer-social">
|
||||
{{range .Config.Footer.Social}}
|
||||
<a href="{{.URL}}" title="{{.Platform}}">
|
||||
{{if eq .Platform "twitter"}}{{svg "octicon-mention" 18}}
|
||||
{{else if eq .Platform "github"}}{{svg "octicon-mark-github" 18}}
|
||||
{{else if eq .Platform "discord"}}{{svg "octicon-comment-discussion" 18}}
|
||||
{{else if eq .Platform "linkedin"}}{{svg "octicon-briefcase" 18}}
|
||||
{{else if eq .Platform "youtube"}}{{svg "octicon-video" 18}}
|
||||
{{else}}{{svg "octicon-link-external" 18}}{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="md-footer-links">
|
||||
{{if .Config.Footer.Links}}
|
||||
{{range .Config.Footer.Links}}
|
||||
<a href="{{.URL}}" class="md-footer-link">{{.Label}}</a>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<a href="{{.Repository.Link}}" class="md-footer-link">Repository</a>
|
||||
<a href="{{.Repository.Link}}/wiki" class="md-footer-link">Documentation</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleAccordion(header) {
|
||||
var item = header.parentElement;
|
||||
var wasOpen = item.classList.contains('open');
|
||||
|
||||
// Close all
|
||||
document.querySelectorAll('.md-accordion-item').forEach(function(el) {
|
||||
el.classList.remove('open');
|
||||
el.querySelector('.md-accordion-icon').textContent = '+';
|
||||
});
|
||||
|
||||
// Open clicked if it was closed
|
||||
if (!wasOpen) {
|
||||
item.classList.add('open');
|
||||
item.querySelector('.md-accordion-icon').textContent = '−';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{{template "pages/base_footer" .}}
|
||||
941
templates/pages/open-source-hero.tmpl
Normal file
@@ -0,0 +1,941 @@
|
||||
{{template "pages/base_head" .}}
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
:root {
|
||||
--osh-bg: #0a0a0b;
|
||||
--osh-text: #e4e4e7;
|
||||
--osh-muted: #71717a;
|
||||
--osh-accent: {{if .Config.Theme.PrimaryColor}}{{.Config.Theme.PrimaryColor}}{{else}}#22c55e{{end}};
|
||||
--osh-accent-dark: {{if .Config.Theme.AccentColor}}{{.Config.Theme.AccentColor}}{{else}}#16a34a{{end}};
|
||||
}
|
||||
|
||||
html, body.pages-body {
|
||||
overflow-x: hidden;
|
||||
background: var(--osh-bg) !important;
|
||||
}
|
||||
|
||||
.osh-page::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.osh-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background: var(--osh-bg);
|
||||
color: var(--osh-text);
|
||||
font-family: 'IBM Plex Sans', -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.osh-hero-gradient {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 150%;
|
||||
height: 100%;
|
||||
background: radial-gradient(ellipse at center, rgba(34, 197, 94, 0.08) 0%, transparent 60%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.osh-grid-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.osh-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(10, 10, 11, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.osh-nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.osh-nav-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, var(--osh-accent) 0%, var(--osh-accent-dark) 100%);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.osh-nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.osh-nav-link {
|
||||
color: var(--osh-muted);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.osh-nav-link:hover {
|
||||
color: var(--osh-text);
|
||||
}
|
||||
|
||||
.osh-btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 28px;
|
||||
background: linear-gradient(135deg, var(--osh-accent) 0%, var(--osh-accent-dark) 100%);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.osh-btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.osh-btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 28px;
|
||||
background: transparent;
|
||||
color: var(--osh-text);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.osh-btn-secondary:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-color: rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
.osh-hero {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 120px 40px 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.osh-hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.osh-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
border-radius: 100px;
|
||||
font-size: 13px;
|
||||
color: var(--osh-accent);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.osh-badge-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--osh-accent);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.osh-hero h1 {
|
||||
font-size: 64px;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 24px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.osh-hero-sub {
|
||||
font-size: 20px;
|
||||
color: #a1a1aa;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 40px;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.osh-hero-ctas {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.osh-code-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 10px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 14px;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.osh-code-block code {
|
||||
color: var(--osh-accent);
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.osh-copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 6px;
|
||||
color: var(--osh-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.osh-copy-btn:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: var(--osh-text);
|
||||
}
|
||||
|
||||
.osh-stats {
|
||||
padding: 60px 40px;
|
||||
border-top: 1px solid rgba(255,255,255,0.05);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.osh-stats-inner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.osh-stat-item {
|
||||
text-align: center;
|
||||
padding: 0 32px;
|
||||
border-right: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.osh-stat-item:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.osh-stat-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: var(--osh-accent);
|
||||
}
|
||||
|
||||
.osh-stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--osh-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.osh-features {
|
||||
padding: 100px 40px;
|
||||
}
|
||||
|
||||
.osh-features-inner {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.osh-section-header {
|
||||
text-align: center;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.osh-section-header h2 {
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.osh-section-header p {
|
||||
font-size: 18px;
|
||||
color: var(--osh-muted);
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.osh-features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.osh-feature-card {
|
||||
padding: 28px;
|
||||
background: rgba(255,255,255,0.02);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.osh-feature-card:hover {
|
||||
background: rgba(255,255,255,0.04);
|
||||
border-color: rgba(34, 197, 94, 0.3);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.osh-feature-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-radius: 10px;
|
||||
color: var(--osh-accent);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.osh-feature-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #fafafa;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.osh-feature-desc {
|
||||
font-size: 14px;
|
||||
color: #a1a1aa;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.osh-social-proof {
|
||||
padding: 80px 40px;
|
||||
background: rgba(255,255,255,0.01);
|
||||
}
|
||||
|
||||
.osh-social-proof-inner {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.osh-logos {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
margin-bottom: 60px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.osh-logo-item {
|
||||
padding: 12px 24px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #52525b;
|
||||
}
|
||||
|
||||
.osh-testimonial {
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.05) 0%, rgba(34, 197, 94, 0.02) 100%);
|
||||
border: 1px solid rgba(34, 197, 94, 0.15);
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.osh-testimonial-quote {
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
color: #fafafa;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.osh-testimonial-author {
|
||||
font-weight: 600;
|
||||
color: var(--osh-text);
|
||||
}
|
||||
|
||||
.osh-testimonial-role {
|
||||
font-size: 14px;
|
||||
color: var(--osh-muted);
|
||||
}
|
||||
|
||||
.osh-readme {
|
||||
padding: 80px 40px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.osh-readme-content {
|
||||
background: rgba(255,255,255,0.02);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.osh-cta-section {
|
||||
padding: 100px 40px;
|
||||
text-align: center;
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(34, 197, 94, 0.03) 100%);
|
||||
border-top: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.osh-cta-inner {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.osh-cta-section h2 {
|
||||
font-size: 44px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.osh-cta-section p {
|
||||
font-size: 18px;
|
||||
color: var(--osh-muted);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.osh-footer {
|
||||
padding: 40px;
|
||||
border-top: 1px solid rgba(255,255,255,0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.osh-footer-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #52525b;
|
||||
}
|
||||
|
||||
.osh-footer-logo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.osh-footer-links {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.osh-footer-link {
|
||||
color: var(--osh-muted);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.osh-footer-link:hover {
|
||||
color: var(--osh-text);
|
||||
}
|
||||
|
||||
.osh-footer-social {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.osh-footer-social a {
|
||||
color: var(--osh-muted);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.osh-footer-social a:hover {
|
||||
color: var(--osh-text);
|
||||
}
|
||||
|
||||
/* Pricing Section */
|
||||
.osh-pricing {
|
||||
padding: 100px 40px;
|
||||
}
|
||||
|
||||
.osh-pricing-inner {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.osh-pricing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.osh-pricing-card {
|
||||
background: rgba(255,255,255,0.02);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.osh-pricing-card:hover {
|
||||
border-color: rgba(255,255,255,0.15);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.osh-pricing-card.featured {
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.08) 0%, rgba(34, 197, 94, 0.02) 100%);
|
||||
border-color: rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.osh-pricing-badge {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 6px 16px;
|
||||
background: linear-gradient(135deg, var(--osh-accent) 0%, var(--osh-accent-dark) 100%);
|
||||
color: #000;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 100px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.osh-pricing-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #fafafa;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.osh-pricing-price {
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
color: var(--osh-text);
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.osh-pricing-period {
|
||||
font-size: 14px;
|
||||
color: var(--osh-muted);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.osh-pricing-features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.osh-pricing-features li {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
font-size: 14px;
|
||||
color: #a1a1aa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.osh-pricing-features li::before {
|
||||
content: "";
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.osh-pricing-features li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.osh-pricing-cta {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 14px 24px;
|
||||
text-align: center;
|
||||
background: rgba(255,255,255,0.05);
|
||||
color: var(--osh-text);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.osh-pricing-cta:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.osh-pricing-card.featured .osh-pricing-cta {
|
||||
background: linear-gradient(135deg, var(--osh-accent) 0%, var(--osh-accent-dark) 100%);
|
||||
color: #000;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.osh-pricing-card.featured .osh-pricing-cta:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.osh-hero h1 { font-size: 40px; }
|
||||
.osh-hero { padding: 100px 24px 60px; }
|
||||
.osh-features, .osh-social-proof, .osh-cta-section, .osh-pricing { padding: 60px 24px; }
|
||||
.osh-pricing-price { font-size: 36px; }
|
||||
.osh-stats { padding: 40px 24px; }
|
||||
.osh-stat-item { border-right: none; padding: 16px 0; }
|
||||
.osh-hero-ctas { flex-direction: column; align-items: center; }
|
||||
.osh-nav { padding: 12px 20px; }
|
||||
.osh-nav-links { gap: 16px; }
|
||||
.osh-section-header h2 { font-size: 32px; }
|
||||
.osh-cta-section h2 { font-size: 32px; }
|
||||
.osh-footer { flex-direction: column; text-align: center; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="osh-page">
|
||||
<!-- Navigation -->
|
||||
<nav class="osh-nav">
|
||||
<a href="/" class="osh-nav-brand">
|
||||
{{if .Config.Brand.LogoURL}}
|
||||
<img src="{{.Config.Brand.LogoURL}}" alt="{{.Config.Brand.Name}}" style="height: 32px;">
|
||||
{{else}}
|
||||
<div class="osh-nav-logo">
|
||||
{{svg "octicon-zap" 16}}
|
||||
</div>
|
||||
{{end}}
|
||||
<span style="font-weight: 600; font-size: 18px;">{{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}</span>
|
||||
</a>
|
||||
<div class="osh-nav-links">
|
||||
{{if .Config.Footer.Links}}
|
||||
{{range .Config.Footer.Links}}
|
||||
<a href="{{.URL}}" class="osh-nav-link">{{.Label}}</a>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<a href="{{.Repository.Link}}" class="osh-nav-link">Repository</a>
|
||||
<a href="{{.Repository.Link}}/wiki" class="osh-nav-link">Docs</a>
|
||||
<a href="{{.Repository.Link}}/releases" class="osh-nav-link">Releases</a>
|
||||
{{end}}
|
||||
{{if .Config.ValueProps}}<a href="#value-props" class="osh-nav-link">Why Us</a>{{end}}
|
||||
{{if .Config.Features}}<a href="#features" class="osh-nav-link">Features</a>{{end}}
|
||||
{{if .Config.Pricing.Plans}}<a href="#pricing" class="osh-nav-link">Pricing</a>{{end}}
|
||||
<a href="{{.Repository.Link}}" class="osh-btn-secondary" style="padding: 10px 20px;">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="18" height="18" alt="GitCaddy">
|
||||
Repository
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="osh-hero">
|
||||
<div class="osh-hero-gradient"></div>
|
||||
<div class="osh-grid-overlay"></div>
|
||||
|
||||
<div class="osh-hero-content">
|
||||
<div class="osh-badge">
|
||||
<span class="osh-badge-dot"></span>
|
||||
{{if .LatestRelease}}v{{.LatestRelease.TagName}} released{{else}}Open Source{{end}}
|
||||
</div>
|
||||
|
||||
<h1>{{if .Config.Hero.Headline}}{{.Config.Hero.Headline}}{{else}}{{.Repository.Name}}{{end}}</h1>
|
||||
|
||||
<p class="osh-hero-sub">
|
||||
{{if .Config.Hero.Subheadline}}{{.Config.Hero.Subheadline}}{{else}}{{.Repository.Description}}{{end}}
|
||||
</p>
|
||||
|
||||
<div class="osh-hero-ctas">
|
||||
{{if .Config.Hero.PrimaryCTA.Label}}
|
||||
<a href="{{.Config.Hero.PrimaryCTA.URL}}" class="osh-btn-primary">
|
||||
{{.Config.Hero.PrimaryCTA.Label}}
|
||||
{{svg "octicon-arrow-right" 16}}
|
||||
</a>
|
||||
{{else}}
|
||||
<a href="{{.Repository.Link}}" class="osh-btn-primary">
|
||||
Get Started
|
||||
{{svg "octicon-arrow-right" 16}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .Config.Hero.SecondaryCTA.Label}}
|
||||
<a href="{{.Config.Hero.SecondaryCTA.URL}}" class="osh-btn-secondary">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="18" height="18" alt="GitCaddy">
|
||||
{{.Config.Hero.SecondaryCTA.Label}}
|
||||
</a>
|
||||
{{else}}
|
||||
<a href="{{.Repository.Link}}" class="osh-btn-secondary">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="18" height="18" alt="GitCaddy">
|
||||
View Source
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Config.Hero.CodeExample}}
|
||||
<div class="osh-code-block">
|
||||
<span style="color: #71717a;">$</span>
|
||||
<code id="install-cmd">{{.Config.Hero.CodeExample}}</code>
|
||||
<button class="osh-copy-btn" onclick="navigator.clipboard.writeText(document.getElementById('install-cmd').textContent)">
|
||||
{{svg "octicon-copy" 16}}
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
{{if or .Config.Stats (gt .NumStars 0)}}
|
||||
<section class="osh-stats">
|
||||
<div class="osh-stats-inner">
|
||||
{{if .Config.Stats}}
|
||||
{{range .Config.Stats}}
|
||||
<div class="osh-stat-item">
|
||||
<div class="osh-stat-value">{{.Value}}</div>
|
||||
<div class="osh-stat-label">{{.Label}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="osh-stat-item">
|
||||
<div class="osh-stat-value">{{.NumStars}}</div>
|
||||
<div class="osh-stat-label">Stars</div>
|
||||
</div>
|
||||
<div class="osh-stat-item">
|
||||
<div class="osh-stat-value">{{.NumForks}}</div>
|
||||
<div class="osh-stat-label">Forks</div>
|
||||
</div>
|
||||
{{if .LatestRelease}}
|
||||
<div class="osh-stat-item">
|
||||
<div class="osh-stat-value">v{{.LatestRelease.TagName}}</div>
|
||||
<div class="osh-stat-label">Latest</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Value Props Section -->
|
||||
{{if .Config.ValueProps}}
|
||||
<section class="osh-features" id="value-props">
|
||||
<div class="osh-features-inner">
|
||||
<div class="osh-section-header">
|
||||
<h2>{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}</h2>
|
||||
<p>Everything you need to get started quickly.</p>
|
||||
</div>
|
||||
<div class="osh-features-grid">
|
||||
{{range .Config.ValueProps}}
|
||||
<div class="osh-feature-card">
|
||||
<div class="osh-feature-icon">
|
||||
{{svg (printf "octicon-%s" (or .Icon "check")) 22}}
|
||||
</div>
|
||||
<h3 class="osh-feature-title">{{.Title}}</h3>
|
||||
<p class="osh-feature-desc">{{.Description}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Features Section -->
|
||||
{{if .Config.Features}}
|
||||
<section class="osh-features" id="features" style="padding-top: 40px;">
|
||||
<div class="osh-features-inner">
|
||||
<div class="osh-section-header">
|
||||
<h2>Features</h2>
|
||||
<p>Powerful capabilities at your fingertips.</p>
|
||||
</div>
|
||||
<div class="osh-features-grid">
|
||||
{{range .Config.Features}}
|
||||
<div class="osh-feature-card">
|
||||
<div class="osh-feature-icon">
|
||||
{{svg (printf "octicon-%s" (or .Icon "zap")) 22}}
|
||||
</div>
|
||||
<h3 class="osh-feature-title">{{.Title}}</h3>
|
||||
<p class="osh-feature-desc">{{.Description}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Social Proof -->
|
||||
{{if or .Config.SocialProof.Logos .Config.SocialProof.Testimonials}}
|
||||
<section class="osh-social-proof">
|
||||
<div class="osh-social-proof-inner">
|
||||
{{if .Config.SocialProof.Logos}}
|
||||
<div class="osh-logos">
|
||||
{{range .Config.SocialProof.Logos}}
|
||||
<div class="osh-logo-item">{{.}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Config.SocialProof.Testimonials}}
|
||||
<div class="osh-testimonials-container">
|
||||
{{range .Config.SocialProof.Testimonials}}
|
||||
<div class="osh-testimonial" style="display: none;">
|
||||
<p class="osh-testimonial-quote">"{{.Quote}}"</p>
|
||||
<div>
|
||||
<div class="osh-testimonial-author">{{.Author}}</div>
|
||||
<div class="osh-testimonial-role">{{.Role}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
var testimonials = document.querySelectorAll(".osh-testimonial");
|
||||
if (testimonials.length > 0) {
|
||||
var idx = Math.floor(Math.random() * testimonials.length);
|
||||
testimonials[idx].style.display = "block";
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Pricing Section -->
|
||||
{{if .Config.Pricing.Plans}}
|
||||
<section class="osh-pricing" id="pricing">
|
||||
<div class="osh-pricing-inner">
|
||||
<div class="osh-section-header">
|
||||
<h2>{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Pricing{{end}}</h2>
|
||||
<p>{{if .Config.Pricing.Subheadline}}{{.Config.Pricing.Subheadline}}{{else}}Choose the plan that works for you{{end}}</p>
|
||||
</div>
|
||||
<div class="osh-pricing-grid">
|
||||
{{range .Config.Pricing.Plans}}
|
||||
<div class="osh-pricing-card{{if .Featured}} featured{{end}}">
|
||||
{{if .Featured}}<span class="osh-pricing-badge">Popular</span>{{end}}
|
||||
<div class="osh-pricing-name">{{.Name}}</div>
|
||||
<div class="osh-pricing-price">{{.Price}}</div>
|
||||
<div class="osh-pricing-period">{{.Period}}</div>
|
||||
{{if .Features}}
|
||||
<ul class="osh-pricing-features">
|
||||
{{range .Features}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
<a href="#" class="osh-pricing-cta">{{if .CTA}}{{.CTA}}{{else}}Get Started{{end}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- CTA Section -->
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<section class="osh-cta-section">
|
||||
<div class="osh-cta-inner">
|
||||
<h2>{{.Config.CTASection.Headline}}</h2>
|
||||
{{if .Config.CTASection.Subheadline}}
|
||||
<p>{{.Config.CTASection.Subheadline}}</p>
|
||||
{{end}}
|
||||
<a href="{{if .Config.CTASection.Button.URL}}{{.Config.CTASection.Button.URL}}{{else}}{{.Repository.Link}}{{end}}" class="osh-btn-primary" style="padding: 16px 32px; font-size: 16px;">
|
||||
{{if .Config.CTASection.Button.Label}}{{.Config.CTASection.Button.Label}}{{else}}Get Started{{end}}
|
||||
{{svg "octicon-arrow-right" 16}}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="osh-footer">
|
||||
<div class="osh-footer-brand">
|
||||
<div class="osh-footer-logo">{{svg "octicon-zap" 14}}</div>
|
||||
<span style="font-size: 14px;">{{if .Config.Footer.Copyright}}{{.Config.Footer.Copyright}}{{else}}© <script>document.write(new Date().getFullYear())</script> {{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}{{end}}</span>
|
||||
</div>
|
||||
{{if .Config.Footer.Social}}
|
||||
<div class="osh-footer-social">
|
||||
{{range .Config.Footer.Social}}
|
||||
<a href="{{.URL}}" title="{{.Platform}}">
|
||||
{{if eq .Platform "twitter"}}{{svg "octicon-mention" 18}}
|
||||
{{else if eq .Platform "github"}}{{svg "octicon-mark-github" 18}}
|
||||
{{else if eq .Platform "discord"}}{{svg "octicon-comment-discussion" 18}}
|
||||
{{else if eq .Platform "linkedin"}}{{svg "octicon-briefcase" 18}}
|
||||
{{else if eq .Platform "youtube"}}{{svg "octicon-video" 18}}
|
||||
{{else}}{{svg "octicon-link-external" 18}}{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="osh-footer-links">
|
||||
{{if .Config.Footer.Links}}
|
||||
{{range .Config.Footer.Links}}
|
||||
<a href="{{.URL}}" class="osh-footer-link">{{.Label}}</a>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<a href="{{.Repository.Link}}" class="osh-footer-link">Repository</a>
|
||||
<a href="{{.Repository.Link}}/wiki" class="osh-footer-link">Documentation</a>
|
||||
<a href="{{.Repository.Link}}/releases" class="osh-footer-link">Releases</a>
|
||||
<a href="{{.Repository.Link}}/issues" class="osh-footer-link">Issues</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{{template "pages/base_footer" .}}
|
||||
@@ -1,198 +0,0 @@
|
||||
{{template "pages/base_head" .}}
|
||||
<style>
|
||||
/* Portfolio-specific styles - Gallery/Creative design */
|
||||
.pages-portfolio {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
}
|
||||
.pages-portfolio .pages-header {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.pages-portfolio .pages-nav-brand,
|
||||
.pages-portfolio .pages-nav-link {
|
||||
color: #fff;
|
||||
}
|
||||
.pages-portfolio .pages-nav-link:hover {
|
||||
color: var(--pages-primary);
|
||||
}
|
||||
.pages-portfolio .pages-hero {
|
||||
background: linear-gradient(180deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
padding: 100px 0 60px;
|
||||
}
|
||||
.pages-portfolio .pages-title {
|
||||
color: #fff;
|
||||
font-size: 2.5rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.pages-portfolio .pages-tagline {
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
.pages-portfolio .pages-logo {
|
||||
border-radius: 50%;
|
||||
border: 3px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
.pages-gallery {
|
||||
padding: 60px 0;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
.pages-gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--columns, 3), 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.pages-gallery-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
aspect-ratio: 1;
|
||||
background: #2d2d2d;
|
||||
}
|
||||
.pages-gallery-item a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.pages-gallery-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease, filter 0.3s ease;
|
||||
}
|
||||
.pages-gallery-item:hover .pages-gallery-image {
|
||||
transform: scale(1.05);
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
.pages-gallery-caption {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
||||
color: #fff;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
.pages-gallery-item:hover .pages-gallery-caption {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.pages-portfolio .pages-readme {
|
||||
background: #2d2d2d;
|
||||
padding: 60px 0;
|
||||
}
|
||||
.pages-portfolio .pages-readme .markup {
|
||||
color: rgba(255,255,255,0.9);
|
||||
}
|
||||
.pages-portfolio .pages-readme .markup h1,
|
||||
.pages-portfolio .pages-readme .markup h2,
|
||||
.pages-portfolio .pages-readme .markup h3 {
|
||||
color: #fff;
|
||||
}
|
||||
.pages-portfolio .pages-readme .markup a {
|
||||
color: var(--pages-primary);
|
||||
}
|
||||
.pages-portfolio .pages-readme .markup code {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #fff;
|
||||
}
|
||||
.pages-portfolio .pages-footer {
|
||||
background: #111;
|
||||
}
|
||||
/* Empty gallery placeholder */
|
||||
.pages-gallery-empty {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
.pages-gallery-empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.pages-gallery-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.pages-gallery-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="pages-landing pages-portfolio">
|
||||
{{template "pages/header" .}}
|
||||
|
||||
<main class="pages-main">
|
||||
<!-- Hero Section -->
|
||||
<section class="pages-hero">
|
||||
<div class="container">
|
||||
{{if .Config.Branding.Logo}}
|
||||
<img src="{{.Config.Branding.Logo}}" alt="{{.Repository.Name}}" class="pages-logo">
|
||||
{{end}}
|
||||
<h1 class="pages-title">{{if .Config.Hero.Title}}{{.Config.Hero.Title}}{{else}}{{.Repository.Name}}{{end}}</h1>
|
||||
{{if .Config.Hero.Tagline}}
|
||||
<p class="pages-tagline">{{.Config.Hero.Tagline}}</p>
|
||||
{{end}}
|
||||
<div class="pages-cta">
|
||||
{{if .Config.Hero.CTAPrimary.Text}}
|
||||
<a href="{{.Config.Hero.CTAPrimary.Link}}" class="ui primary button">
|
||||
{{.Config.Hero.CTAPrimary.Text}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Config.Hero.CTASecondary.Text}}
|
||||
<a href="{{.Config.Hero.CTASecondary.Link}}" class="ui button basic inverted">
|
||||
{{.Config.Hero.CTASecondary.Text}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gallery Section -->
|
||||
<section class="pages-gallery">
|
||||
<div class="container">
|
||||
{{if .Config.Gallery.Items}}
|
||||
<div class="pages-gallery-grid" style="--columns: {{if .Config.Gallery.Columns}}{{.Config.Gallery.Columns}}{{else}}3{{end}}">
|
||||
{{range .Config.Gallery.Items}}
|
||||
<div class="pages-gallery-item">
|
||||
{{if .Link}}<a href="{{.Link}}">{{end}}
|
||||
<img src="{{.Image}}" alt="{{.Title}}" class="pages-gallery-image">
|
||||
{{if .Title}}
|
||||
<div class="pages-gallery-caption">{{.Title}}</div>
|
||||
{{end}}
|
||||
{{if .Link}}</a>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="pages-gallery-grid" style="--columns: 3">
|
||||
<div class="pages-gallery-empty">
|
||||
{{svg "octicon-image" 48}}
|
||||
<p>Add gallery items in your landing.yaml configuration</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- README Section -->
|
||||
{{if .ReadmeContent}}
|
||||
<section class="pages-readme">
|
||||
<div class="container">
|
||||
<div class="markup">
|
||||
{{.ReadmeContent}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
</main>
|
||||
|
||||
{{template "pages/footer" .}}
|
||||
</div>
|
||||
{{template "pages/base_footer" .}}
|
||||
@@ -1,198 +0,0 @@
|
||||
{{template "pages/base_head" .}}
|
||||
<style>
|
||||
/* Product-specific styles - Bold marketing design */
|
||||
.pages-product {
|
||||
background: #fff;
|
||||
}
|
||||
.pages-product .pages-header {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
.pages-product .pages-nav-brand,
|
||||
.pages-product .pages-nav-link {
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
|
||||
}
|
||||
.pages-hero-product {
|
||||
background: linear-gradient(135deg, var(--pages-primary) 0%, #1a365d 100%);
|
||||
color: #fff;
|
||||
padding: 120px 0 80px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.pages-hero-product::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
.pages-hero-product .container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.pages-product .pages-title {
|
||||
color: #fff;
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.pages-product .pages-tagline {
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-size: 1.5rem;
|
||||
max-width: 700px;
|
||||
}
|
||||
.pages-product .pages-logo {
|
||||
max-height: 100px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
.pages-features {
|
||||
padding: 80px 0;
|
||||
background: #fff;
|
||||
}
|
||||
.pages-features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 48px;
|
||||
}
|
||||
.pages-feature {
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
}
|
||||
.pages-feature-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 24px;
|
||||
background: linear-gradient(135deg, var(--pages-primary) 0%, #1a365d 100%);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
.pages-feature-icon img {
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
.pages-feature-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px;
|
||||
color: #24292e;
|
||||
}
|
||||
.pages-feature-description {
|
||||
color: #586069;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
.pages-product .pages-stats {
|
||||
background: linear-gradient(135deg, #f6f8fa 0%, #e1e4e8 100%);
|
||||
padding: 64px 0;
|
||||
}
|
||||
.pages-cta-bottom {
|
||||
margin-top: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
.pages-product .pages-readme {
|
||||
background: #fff;
|
||||
padding: 80px 0;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.pages-product .pages-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.pages-hero-product {
|
||||
padding: 100px 0 60px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="pages-landing pages-product">
|
||||
{{template "pages/header" .}}
|
||||
|
||||
<main class="pages-main">
|
||||
<!-- Hero Section -->
|
||||
<section class="pages-hero pages-hero-product" {{if .Config.Hero.Background}}style="background-image: url('{{.Config.Hero.Background}}')"{{end}}>
|
||||
<div class="container">
|
||||
{{if .Config.Branding.Logo}}
|
||||
<img src="{{.Config.Branding.Logo}}" alt="{{.Repository.Name}}" class="pages-logo">
|
||||
{{end}}
|
||||
<h1 class="pages-title">{{if .Config.Hero.Title}}{{.Config.Hero.Title}}{{else}}{{.Repository.Name}}{{end}}</h1>
|
||||
{{if .Config.Hero.Tagline}}
|
||||
<p class="pages-tagline">{{.Config.Hero.Tagline}}</p>
|
||||
{{end}}
|
||||
<div class="pages-cta">
|
||||
{{if .Config.Hero.CTAPrimary.Text}}
|
||||
<a href="{{.Config.Hero.CTAPrimary.Link}}" class="ui large primary button" style="background: #fff; color: var(--pages-primary);">
|
||||
{{.Config.Hero.CTAPrimary.Text}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Config.Hero.CTASecondary.Text}}
|
||||
<a href="{{.Config.Hero.CTASecondary.Link}}" class="ui large button basic inverted">
|
||||
{{.Config.Hero.CTASecondary.Text}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
{{if .Config.Features}}
|
||||
<section class="pages-features">
|
||||
<div class="container">
|
||||
<div class="pages-features-grid">
|
||||
{{range .Config.Features}}
|
||||
<div class="pages-feature">
|
||||
{{if .Icon}}
|
||||
<div class="pages-feature-icon">
|
||||
{{svg (printf "octicon-%s" .Icon) 32}}
|
||||
</div>
|
||||
{{end}}
|
||||
<h3 class="pages-feature-title">{{.Title}}</h3>
|
||||
<p class="pages-feature-description">{{.Description}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- README Section -->
|
||||
{{if .ReadmeContent}}
|
||||
<section class="pages-readme">
|
||||
<div class="container">
|
||||
<div class="markup">
|
||||
{{.ReadmeContent}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="pages-stats">
|
||||
<div class="container">
|
||||
<div class="pages-stats-grid">
|
||||
<div class="pages-stat">
|
||||
<span class="pages-stat-value">{{.NumStars}}</span>
|
||||
<span class="pages-stat-label">{{ctx.Locale.Tr "repo.stars"}}</span>
|
||||
</div>
|
||||
<div class="pages-stat">
|
||||
<span class="pages-stat-value">{{.NumForks}}</span>
|
||||
<span class="pages-stat-label">{{ctx.Locale.Tr "repo.forks"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pages-cta-bottom">
|
||||
<a href="{{AppSubUrl}}/{{.Repository.FullName}}" class="ui large button" target="_blank">
|
||||
{{svg "octicon-star" 16}} Star on Gitea
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{{template "pages/footer" .}}
|
||||
</div>
|
||||
{{template "pages/base_footer" .}}
|
||||
937
templates/pages/saas-conversion.tmpl
Normal file
@@ -0,0 +1,937 @@
|
||||
{{template "pages/base_head" .}}
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--saas-bg: #fafbfc;
|
||||
--saas-text: #1a1a2e;
|
||||
--saas-muted: #64748b;
|
||||
--saas-light: #94a3b8;
|
||||
--saas-border: #e5e7eb;
|
||||
--saas-primary: {{if .Config.Theme.PrimaryColor}}{{.Config.Theme.PrimaryColor}}{{else}}#6366f1{{end}};
|
||||
--saas-primary-dark: {{if .Config.Theme.AccentColor}}{{.Config.Theme.AccentColor}}{{else}}#8b5cf6{{end}};
|
||||
}
|
||||
|
||||
.saas-page {
|
||||
min-height: 100vh;
|
||||
background: var(--saas-bg);
|
||||
color: var(--saas-text);
|
||||
font-family: 'DM Sans', -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.saas-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(250, 251, 252, 0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.saas-nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.saas-nav-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, var(--saas-primary) 0%, var(--saas-primary-dark) 100%);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.saas-nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 36px;
|
||||
}
|
||||
|
||||
.saas-nav-link {
|
||||
color: var(--saas-muted);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.saas-nav-link:hover {
|
||||
color: var(--saas-text);
|
||||
}
|
||||
|
||||
.saas-nav-repo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--saas-muted);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.saas-nav-repo:hover {
|
||||
color: var(--saas-text);
|
||||
}
|
||||
|
||||
.saas-btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 32px;
|
||||
background: linear-gradient(135deg, var(--saas-primary) 0%, var(--saas-primary-dark) 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.saas-btn-primary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.45);
|
||||
}
|
||||
|
||||
.saas-btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 32px;
|
||||
background: white;
|
||||
color: var(--saas-text);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
border: 2px solid var(--saas-border);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.saas-btn-secondary:hover {
|
||||
border-color: var(--saas-primary);
|
||||
color: var(--saas-primary);
|
||||
}
|
||||
|
||||
.saas-hero {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 140px 48px 80px;
|
||||
background: linear-gradient(180deg, var(--saas-bg) 0%, #f1f5f9 100%);
|
||||
}
|
||||
|
||||
.saas-hero-inner {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 80px;
|
||||
max-width: 1300px;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.saas-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: #ede9fe;
|
||||
border-radius: 100px;
|
||||
margin-bottom: 28px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--saas-primary);
|
||||
}
|
||||
|
||||
.saas-hero h1 {
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
margin-bottom: 24px;
|
||||
color: var(--saas-text);
|
||||
}
|
||||
|
||||
.saas-hero-sub {
|
||||
font-size: 20px;
|
||||
color: var(--saas-muted);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.saas-hero-ctas {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.saas-hero-note {
|
||||
font-size: 14px;
|
||||
color: var(--saas-light);
|
||||
}
|
||||
|
||||
.saas-hero-mockup {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.12);
|
||||
padding: 24px;
|
||||
animation: saas-float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes saas-float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.saas-mockup-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.saas-mockup-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.saas-stats-section {
|
||||
padding: 60px 48px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.saas-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 32px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.saas-stat-value {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--saas-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.saas-stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--saas-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.saas-trust-section {
|
||||
padding: 60px 48px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.saas-trust-label {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--saas-light);
|
||||
margin-bottom: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.saas-trust-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 48px;
|
||||
padding: 32px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.04);
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.saas-trust-logo {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--saas-light);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.saas-trust-logo:hover {
|
||||
color: var(--saas-muted);
|
||||
}
|
||||
|
||||
.saas-section {
|
||||
padding: 100px 48px;
|
||||
}
|
||||
|
||||
.saas-section-inner {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.saas-section-header {
|
||||
text-align: center;
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
|
||||
.saas-section-header h2 {
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
color: var(--saas-text);
|
||||
}
|
||||
|
||||
.saas-section-header p {
|
||||
font-size: 18px;
|
||||
color: var(--saas-muted);
|
||||
}
|
||||
|
||||
.saas-value-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.saas-value-card {
|
||||
padding: 32px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.04);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.saas-value-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.08);
|
||||
border-color: var(--saas-border);
|
||||
}
|
||||
|
||||
.saas-value-card h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
color: var(--saas-text);
|
||||
}
|
||||
|
||||
.saas-value-card p {
|
||||
font-size: 16px;
|
||||
color: var(--saas-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.saas-features-section {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.saas-features-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.saas-feature-step {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 32px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--saas-border);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.saas-feature-step:hover {
|
||||
border-color: var(--saas-primary);
|
||||
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.saas-step-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #ede9fe 0%, #e0e7ff 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--saas-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.saas-feature-step h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--saas-text);
|
||||
}
|
||||
|
||||
.saas-feature-step p {
|
||||
font-size: 16px;
|
||||
color: var(--saas-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.saas-testimonial-card {
|
||||
background: white;
|
||||
border-radius: 24px;
|
||||
padding: 48px;
|
||||
box-shadow: 0 4px 30px rgba(0,0,0,0.06);
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.saas-testimonial-card::before {
|
||||
content: '"';
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 40px;
|
||||
font-size: 120px;
|
||||
font-weight: 700;
|
||||
color: #ede9fe;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.saas-testimonial-quote {
|
||||
font-size: 24px;
|
||||
line-height: 1.7;
|
||||
color: var(--saas-text);
|
||||
margin-bottom: 32px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.saas-testimonial-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.saas-testimonial-avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, var(--saas-primary) 0%, var(--saas-primary-dark) 100%);
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.saas-testimonial-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.saas-testimonial-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: var(--saas-text);
|
||||
}
|
||||
|
||||
.saas-testimonial-role {
|
||||
font-size: 14px;
|
||||
color: var(--saas-muted);
|
||||
}
|
||||
|
||||
.saas-pricing-section {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.saas-pricing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.saas-pricing-card {
|
||||
padding: 36px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
border: 2px solid var(--saas-border);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.saas-pricing-card:hover {
|
||||
border-color: #c7d2fe;
|
||||
}
|
||||
|
||||
.saas-pricing-card.featured {
|
||||
border-color: var(--saas-primary);
|
||||
box-shadow: 0 8px 40px rgba(99, 102, 241, 0.15);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.saas-pricing-badge {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
background: linear-gradient(135deg, var(--saas-primary) 0%, var(--saas-primary-dark) 100%);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 100px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.saas-pricing-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--saas-text);
|
||||
}
|
||||
|
||||
.saas-pricing-price {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: var(--saas-text);
|
||||
}
|
||||
|
||||
.saas-pricing-period {
|
||||
color: var(--saas-muted);
|
||||
}
|
||||
|
||||
.saas-pricing-features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 24px 0 28px;
|
||||
}
|
||||
|
||||
.saas-pricing-features li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--saas-muted);
|
||||
}
|
||||
|
||||
.saas-pricing-features li::before {
|
||||
content: "";
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.saas-cta-gradient {
|
||||
background: linear-gradient(135deg, var(--saas-primary) 0%, var(--saas-primary-dark) 50%, #a855f7 100%);
|
||||
border-radius: 32px;
|
||||
padding: 80px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.saas-cta-gradient::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
|
||||
animation: saas-float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.saas-cta-gradient h2 {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.saas-cta-gradient p {
|
||||
font-size: 20px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 32px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.saas-cta-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 18px 36px;
|
||||
background: white;
|
||||
color: var(--saas-primary);
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.saas-cta-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.saas-footer {
|
||||
padding: 48px;
|
||||
border-top: 1px solid var(--saas-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.saas-footer-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.saas-footer-logo {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: linear-gradient(135deg, var(--saas-primary) 0%, var(--saas-primary-dark) 100%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.saas-footer-links {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.saas-footer-link {
|
||||
color: var(--saas-muted);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.saas-footer-link:hover {
|
||||
color: var(--saas-primary);
|
||||
}
|
||||
|
||||
.saas-footer-social {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.saas-footer-social a {
|
||||
color: var(--saas-light);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.saas-footer-social a:hover {
|
||||
color: var(--saas-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.saas-hero-inner { grid-template-columns: 1fr; }
|
||||
.saas-hero-mockup { display: none; }
|
||||
.saas-pricing-grid { max-width: 400px; margin: 0 auto; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.saas-hero h1 { font-size: 40px; }
|
||||
.saas-hero-ctas { flex-direction: column; }
|
||||
.saas-nav { padding: 12px 24px; }
|
||||
.saas-nav-links { gap: 16px; display: none; }
|
||||
.saas-section { padding: 60px 24px; }
|
||||
.saas-stats-section, .saas-trust-section { padding: 40px 24px; }
|
||||
.saas-cta-gradient { padding: 48px 24px; margin: 0 16px; }
|
||||
.saas-footer { flex-direction: column; text-align: center; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="saas-page">
|
||||
<!-- Navigation -->
|
||||
<nav class="saas-nav">
|
||||
<a href="/" class="saas-nav-brand">
|
||||
{{if .Config.Brand.LogoURL}}
|
||||
<img src="{{.Config.Brand.LogoURL}}" alt="{{.Config.Brand.Name}}" style="height: 36px;">
|
||||
{{else}}
|
||||
<div class="saas-nav-logo">
|
||||
{{svg "octicon-package" 18}}
|
||||
</div>
|
||||
{{end}}
|
||||
<span style="font-weight: 700; font-size: 20px; color: var(--saas-text);">{{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}</span>
|
||||
</a>
|
||||
<div class="saas-nav-links">
|
||||
{{if .Config.Footer.Links}}
|
||||
{{range .Config.Footer.Links}}
|
||||
<a href="{{.URL}}" class="saas-nav-link">{{.Label}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .Config.ValueProps}}<a href="#value-props" class="saas-nav-link">Why</a>{{end}}
|
||||
{{if .Config.Features}}<a href="#features" class="saas-nav-link">Features</a>{{end}}
|
||||
{{if .Config.Pricing.Plans}}<a href="#pricing" class="saas-nav-link">Pricing</a>{{end}}
|
||||
<a href="{{.Repository.Link}}" class="saas-nav-repo">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="18" height="18" alt="GitCaddy">
|
||||
Repository
|
||||
</a>
|
||||
<a href="{{if .Config.Hero.PrimaryCTA.URL}}{{.Config.Hero.PrimaryCTA.URL}}{{else}}{{.Repository.Link}}{{end}}" class="saas-btn-primary" style="padding: 12px 24px; font-size: 14px;">
|
||||
{{if .Config.Hero.PrimaryCTA.Label}}{{.Config.Hero.PrimaryCTA.Label}}{{else}}Get Started{{end}}
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="saas-hero">
|
||||
<div class="saas-hero-inner">
|
||||
<div>
|
||||
{{if gt .NumStars 100}}
|
||||
<div class="saas-badge">
|
||||
{{svg "octicon-star-fill" 14}}
|
||||
<span>{{.NumStars}} stars</span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<h1>{{if .Config.Hero.Headline}}{{.Config.Hero.Headline}}{{else}}{{.Repository.Name}}{{end}}</h1>
|
||||
|
||||
<p class="saas-hero-sub">
|
||||
{{if .Config.Hero.Subheadline}}{{.Config.Hero.Subheadline}}{{else}}{{.Repository.Description}}{{end}}
|
||||
</p>
|
||||
|
||||
<div class="saas-hero-ctas">
|
||||
<a href="{{if .Config.Hero.PrimaryCTA.URL}}{{.Config.Hero.PrimaryCTA.URL}}{{else}}{{.Repository.Link}}{{end}}" class="saas-btn-primary">
|
||||
{{if .Config.Hero.PrimaryCTA.Label}}{{.Config.Hero.PrimaryCTA.Label}}{{else}}Get Started{{end}}
|
||||
{{svg "octicon-arrow-right" 16}}
|
||||
</a>
|
||||
{{if .Config.Hero.SecondaryCTA.Label}}
|
||||
<a href="{{.Config.Hero.SecondaryCTA.URL}}" class="saas-btn-secondary">
|
||||
{{svg "octicon-play" 16}}
|
||||
{{.Config.Hero.SecondaryCTA.Label}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<p class="saas-hero-note">
|
||||
{{svg "octicon-check" 14}} Free to use {{svg "octicon-check" 14}} Open source {{svg "octicon-check" 14}} MIT License
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="saas-hero-mockup">
|
||||
<div class="saas-mockup-bar">
|
||||
<div class="saas-mockup-dot" style="background: #ef4444;"></div>
|
||||
<div class="saas-mockup-dot" style="background: #f59e0b;"></div>
|
||||
<div class="saas-mockup-dot" style="background: #22c55e;"></div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<div style="height: 32px; background: #f1f5f9; border-radius: 8px; width: 60%;"></div>
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;">
|
||||
<div style="height: 80px; background: linear-gradient(135deg, #ede9fe 0%, #e0e7ff 100%); border-radius: 10px;"></div>
|
||||
<div style="height: 80px; background: #f1f5f9; border-radius: 10px;"></div>
|
||||
<div style="height: 80px; background: #f1f5f9; border-radius: 10px;"></div>
|
||||
</div>
|
||||
<div style="height: 120px; background: linear-gradient(135deg, var(--saas-primary) 0%, var(--saas-primary-dark) 100%); border-radius: 12px; opacity: 0.15;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
{{if .Config.Stats}}
|
||||
<section class="saas-stats-section">
|
||||
<div class="saas-stats-grid">
|
||||
{{range .Config.Stats}}
|
||||
<div>
|
||||
<div class="saas-stat-value">{{.Value}}</div>
|
||||
<div class="saas-stat-label">{{.Label}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Trust Bar -->
|
||||
{{if .Config.SocialProof.Logos}}
|
||||
<section class="saas-trust-section">
|
||||
<p class="saas-trust-label">TRUSTED BY TEAMS AT</p>
|
||||
<div class="saas-trust-bar">
|
||||
{{range .Config.SocialProof.Logos}}
|
||||
<span class="saas-trust-logo">{{.}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Value Props -->
|
||||
{{if .Config.ValueProps}}
|
||||
<section class="saas-section" id="value-props">
|
||||
<div class="saas-section-inner">
|
||||
<div class="saas-section-header">
|
||||
<h2>Why {{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}</h2>
|
||||
<p>Built for developers who value their time</p>
|
||||
</div>
|
||||
|
||||
<div class="saas-value-grid">
|
||||
{{range .Config.ValueProps}}
|
||||
<div class="saas-value-card">
|
||||
<h3>{{.Title}}</h3>
|
||||
<p>{{.Description}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Features -->
|
||||
{{if .Config.Features}}
|
||||
<section id="features" class="saas-section saas-features-section">
|
||||
<div class="saas-section-inner">
|
||||
<div class="saas-section-header">
|
||||
<h2>How it works</h2>
|
||||
<p>Get started in minutes, not weeks</p>
|
||||
</div>
|
||||
|
||||
<div class="saas-features-list">
|
||||
{{range .Config.Features}}
|
||||
<div class="saas-feature-step">
|
||||
<div class="saas-step-icon">
|
||||
{{svg (printf "octicon-%s" (or .Icon "zap")) 24}}
|
||||
</div>
|
||||
<div>
|
||||
<h3>{{.Title}}</h3>
|
||||
<p>{{.Description}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Testimonials -->
|
||||
{{if .Config.SocialProof.Testimonials}}
|
||||
<section class="saas-section">
|
||||
<div class="saas-testimonial-card">
|
||||
<div class="saas-testimonials-container">
|
||||
{{range .Config.SocialProof.Testimonials}}
|
||||
<div class="saas-testimonial-item" style="display: none;">
|
||||
<p class="saas-testimonial-quote">{{.Quote}}</p>
|
||||
<div class="saas-testimonial-author">
|
||||
<div class="saas-testimonial-avatar">
|
||||
{{if .Avatar}}<img src="{{.Avatar}}" alt="{{.Author}}">{{end}}
|
||||
</div>
|
||||
<div>
|
||||
<div class="saas-testimonial-name">{{.Author}}</div>
|
||||
<div class="saas-testimonial-role">{{.Role}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function() {
|
||||
var items = document.querySelectorAll(".saas-testimonial-item");
|
||||
if (items.length > 0) {
|
||||
var idx = Math.floor(Math.random() * items.length);
|
||||
items[idx].style.display = "block";
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<!-- Pricing -->
|
||||
{{if .Config.Pricing.Plans}}
|
||||
<section id="pricing" class="saas-section saas-pricing-section">
|
||||
<div class="saas-section-inner">
|
||||
<div class="saas-section-header">
|
||||
<h2>{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Simple, transparent pricing{{end}}</h2>
|
||||
</div>
|
||||
|
||||
<div class="saas-pricing-grid">
|
||||
{{range .Config.Pricing.Plans}}
|
||||
<div class="saas-pricing-card {{if .Featured}}featured{{end}}">
|
||||
{{if .Featured}}<div class="saas-pricing-badge">Most Popular</div>{{end}}
|
||||
<h3 class="saas-pricing-name">{{.Name}}</h3>
|
||||
<div style="margin-bottom: 24px;">
|
||||
<span class="saas-pricing-price">{{.Price}}</span>
|
||||
<span class="saas-pricing-period">{{.Period}}</span>
|
||||
</div>
|
||||
{{if .Features}}
|
||||
<ul class="saas-pricing-features">
|
||||
{{range .Features}}
|
||||
<li>{{svg "octicon-check" 16}} {{.}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
<a href="#" class="{{if .Featured}}saas-btn-primary{{else}}saas-btn-secondary{{end}}" style="width: 100%; justify-content: center;">
|
||||
{{if .CTA}}{{.CTA}}{{else}}Get Started{{end}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Final CTA -->
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<section class="saas-section">
|
||||
<div class="saas-cta-gradient">
|
||||
<h2>{{.Config.CTASection.Headline}}</h2>
|
||||
{{if .Config.CTASection.Subheadline}}
|
||||
<p>{{.Config.CTASection.Subheadline}}</p>
|
||||
{{end}}
|
||||
<a href="{{if .Config.CTASection.Button.URL}}{{.Config.CTASection.Button.URL}}{{else}}{{.Repository.Link}}{{end}}" class="saas-cta-btn">
|
||||
{{if .Config.CTASection.Button.Label}}{{.Config.CTASection.Button.Label}}{{else}}Get Started Free{{end}}
|
||||
{{svg "octicon-arrow-right" 16}}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="saas-footer">
|
||||
<div class="saas-footer-brand">
|
||||
<div class="saas-footer-logo"></div>
|
||||
<span style="font-size: 14px; color: var(--saas-light);">{{if .Config.Footer.Copyright}}{{.Config.Footer.Copyright}}{{else}}© <script>document.write(new Date().getFullYear())</script> {{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}{{end}}</span>
|
||||
</div>
|
||||
{{if .Config.Footer.Social}}
|
||||
<div class="saas-footer-social">
|
||||
{{range .Config.Footer.Social}}
|
||||
<a href="{{.URL}}" title="{{.Platform}}">
|
||||
{{if eq .Platform "twitter"}}{{svg "octicon-mention" 18}}
|
||||
{{else if eq .Platform "github"}}{{svg "octicon-mark-github" 18}}
|
||||
{{else if eq .Platform "discord"}}{{svg "octicon-comment-discussion" 18}}
|
||||
{{else if eq .Platform "linkedin"}}{{svg "octicon-briefcase" 18}}
|
||||
{{else if eq .Platform "youtube"}}{{svg "octicon-video" 18}}
|
||||
{{else}}{{svg "octicon-link-external" 18}}{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="saas-footer-links">
|
||||
{{if .Config.Footer.Links}}
|
||||
{{range .Config.Footer.Links}}
|
||||
<a href="{{.URL}}" class="saas-footer-link">{{.Label}}</a>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<a href="{{.Repository.Link}}" class="saas-footer-link">Repository</a>
|
||||
<a href="{{.Repository.Link}}/wiki" class="saas-footer-link">Docs</a>
|
||||
<a href="{{.Repository.Link}}/releases" class="saas-footer-link">Releases</a>
|
||||
<a href="{{.Repository.Link}}/issues" class="saas-footer-link">Issues</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{{template "pages/base_footer" .}}
|
||||
@@ -1,66 +0,0 @@
|
||||
{{template "pages/base_head" .}}
|
||||
<div class="pages-landing pages-simple">
|
||||
{{template "pages/header" .}}
|
||||
|
||||
<main class="pages-main">
|
||||
{{if .Config.Hero.Title}}
|
||||
<section class="pages-hero">
|
||||
<div class="container">
|
||||
{{if .Config.Branding.Logo}}
|
||||
<img src="{{.Config.Branding.Logo}}" alt="{{.Repository.Name}}" class="pages-logo">
|
||||
{{end}}
|
||||
<h1 class="pages-title">{{.Config.Hero.Title}}</h1>
|
||||
{{if .Config.Hero.Tagline}}
|
||||
<p class="pages-tagline">{{.Config.Hero.Tagline}}</p>
|
||||
{{end}}
|
||||
<div class="pages-cta">
|
||||
{{if .Config.Hero.CTAPrimary.Text}}
|
||||
<a href="{{.Config.Hero.CTAPrimary.Link}}" class="ui primary button">
|
||||
{{.Config.Hero.CTAPrimary.Text}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Config.Hero.CTASecondary.Text}}
|
||||
<a href="{{.Config.Hero.CTASecondary.Link}}" class="ui button {{if eq .Config.Hero.CTASecondary.Style "outline"}}basic{{end}}">
|
||||
{{.Config.Hero.CTASecondary.Text}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{if .ReadmeContent}}
|
||||
<section class="pages-readme">
|
||||
<div class="container">
|
||||
<div class="markup">
|
||||
{{.ReadmeContent}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<section class="pages-stats">
|
||||
<div class="container">
|
||||
<div class="pages-stats-grid">
|
||||
<div class="pages-stat">
|
||||
<span class="pages-stat-value">{{.NumStars}}</span>
|
||||
<span class="pages-stat-label">{{ctx.Locale.Tr "repo.stars"}}</span>
|
||||
</div>
|
||||
<div class="pages-stat">
|
||||
<span class="pages-stat-value">{{.NumForks}}</span>
|
||||
<span class="pages-stat-label">{{ctx.Locale.Tr "repo.forks"}}</span>
|
||||
</div>
|
||||
{{if .Repository.PrimaryLanguage}}
|
||||
<div class="pages-stat">
|
||||
<span class="pages-stat-value">{{.Repository.PrimaryLanguage.Language}}</span>
|
||||
<span class="pages-stat-label">{{ctx.Locale.Tr "repo.language"}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{{template "pages/footer" .}}
|
||||
</div>
|
||||
{{template "pages/base_footer" .}}
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title tw-text-18">
|
||||
<a class="muted tw-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/<a class="muted" href="{{$.RepoLink}}">{{.Name}}</a>
|
||||
<a class="muted tw-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/<a class="muted" href="{{$.RepoLink}}">{{if .DisplayTitle}}{{.DisplayTitle}}{{else}}{{.Name}}{{end}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
@@ -173,7 +173,7 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypePackages}}
|
||||
{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypePackages) (or .IsWriter .HasPackages)}}
|
||||
<a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item">
|
||||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
|
||||
</a>
|
||||
@@ -198,7 +198,7 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeWiki}}
|
||||
{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeWiki) (or .IsWriter .HasWiki)}}
|
||||
<a class="{{if .PageIsWiki}}active {{end}}item" href="{{.RepoLink}}/wiki">
|
||||
{{svg "octicon-book"}} {{ctx.Locale.Tr "repo.wiki"}}
|
||||
</a>
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .DetectedRepoLicenses}}
|
||||
{{if .Repository.LicenseType}}
|
||||
<a class="flex-text-block muted" href="{{.RepoLink}}/src/{{.Repository.DefaultBranch}}/LICENSE.md" title="{{.Repository.LicenseType}}">
|
||||
{{svg "octicon-law"}} {{.Repository.LicenseType}}
|
||||
</a>
|
||||
{{else if .DetectedRepoLicenses}}
|
||||
<a class="flex-text-block muted" href="{{.RepoLink}}/src/{{.Repository.DefaultBranch}}/{{PathEscapeSegments .LicenseFileName}}" title="{{StringUtils.Join .DetectedRepoLicenses ", "}}">
|
||||
{{svg "octicon-law"}} {{if eq (len .DetectedRepoLicenses) 1}}{{index .DetectedRepoLicenses 0}}{{else}}{{ctx.Locale.Tr "repo.multiple_licenses"}}{{end}}
|
||||
</a>
|
||||
|
||||
36
templates/repo/settings/license.tmpl
Normal file
@@ -0,0 +1,36 @@
|
||||
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings license")}}
|
||||
<div class="repo-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "repo.settings.license"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.license_type"}}</label>
|
||||
<select name="license_type" class="ui dropdown">
|
||||
<option value="">{{ctx.Locale.Tr "repo.settings.license_none"}}</option>
|
||||
{{range .LicenseTypes}}
|
||||
<optgroup label="{{.Category}}">
|
||||
{{range .Licenses}}
|
||||
<option value="{{.Key}}" {{if eq $.Repository.LicenseType .Key}}selected{{end}} title="{{.Description}}">{{.Name}}</option>
|
||||
{{end}}
|
||||
</optgroup>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
{{if .Repository.LicenseType}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.current_license"}}</label>
|
||||
<p><strong>{{.Repository.LicenseType}}</strong></p>
|
||||
<p class="help">
|
||||
<a href="{{.RepoLink}}/src/{{.Repository.DefaultBranch}}/LICENSE.md">{{ctx.Locale.Tr "repo.settings.view_license_file"}}</a>
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="help tw-mb-4">{{ctx.Locale.Tr "repo.settings.license_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{template "repo/settings/layout_footer" .}}
|
||||