Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 416278c747 | |||
| 58cf5dd410 | |||
| 0ab62c2b95 | |||
| 97b534f66b | |||
| eb969b8f82 | |||
| b310e8ed18 | |||
| e8286de3be | |||
| 4648a021c5 | |||
| 1a335e741b | |||
| 998337e80d | |||
| 009566a3ce | |||
| 2ca88587d4 | |||
| 05f8df8a9e | |||
| 44b6c62093 | |||
| 4557edfe98 | |||
| 3b8cfc0c03 | |||
| 55568524a7 | |||
| 62f4a3ce37 | |||
| 3c8405a3b2 | |||
| e105e047a4 | |||
| f9f2d45c13 | |||
| 3a62a5d8c1 | |||
| f2f4367dbe | |||
| 7b4e85a473 | |||
| d8904e2846 | |||
| 4fabef6a65 | |||
| f26bd3e273 | |||
| c5daac3366 | |||
| 916211004d | |||
| 02fdc1a194 | |||
| 1b0bba09b9 | |||
| 0c0d1c1493 | |||
| 9461599b57 | |||
| 414560f470 | |||
| b43345986a | |||
| 7fbbd26b20 | |||
| b26bf4bfe8 |
@@ -16,7 +16,7 @@ env:
|
||||
GOPRIVATE: git.marketally.com
|
||||
GONOSUMDB: git.marketally.com
|
||||
GOTOOLCHAIN: local
|
||||
GO_VERSION: "1.25.5"
|
||||
GO_VERSION: "1.25.9"
|
||||
NODE_VERSION: "22"
|
||||
|
||||
jobs:
|
||||
@@ -24,6 +24,8 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: linux-latest
|
||||
env:
|
||||
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -51,7 +53,8 @@ jobs:
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: make deps-frontend deps-backend
|
||||
run: |
|
||||
make deps-frontend deps-backend
|
||||
|
||||
- name: Run Go linter
|
||||
run: make lint-go
|
||||
@@ -64,6 +67,8 @@ jobs:
|
||||
test-unit:
|
||||
name: Unit Tests
|
||||
runs-on: linux-latest
|
||||
env:
|
||||
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-unit
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -83,7 +88,8 @@ jobs:
|
||||
cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
run: |
|
||||
go mod download
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
@@ -99,6 +105,8 @@ jobs:
|
||||
test-pgsql:
|
||||
name: Integration Tests (PostgreSQL)
|
||||
runs-on: linux-latest
|
||||
env:
|
||||
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-pgsql
|
||||
services:
|
||||
pgsql:
|
||||
image: postgres:15
|
||||
@@ -140,7 +148,8 @@ jobs:
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: make deps-frontend deps-backend
|
||||
run: |
|
||||
make deps-frontend deps-backend
|
||||
|
||||
- name: Build frontend
|
||||
run: make frontend
|
||||
@@ -351,7 +360,7 @@ jobs:
|
||||
sed -i "s|replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault|replace git.marketally.com/gitcaddy/gitcaddy-vault => git.marketally.com/gitcaddy/gitcaddy-vault $VAULT_VERSION|" go.mod
|
||||
fi
|
||||
cat go.mod | grep -A2 "gitcaddy-vault" || true
|
||||
go mod tidy -v 2>&1
|
||||
go mod download
|
||||
|
||||
- name: Update vault dependency (Windows)
|
||||
if: matrix.goos == 'windows'
|
||||
@@ -366,7 +375,7 @@ jobs:
|
||||
$content = $content -replace 'replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault', "replace git.marketally.com/gitcaddy/gitcaddy-vault => git.marketally.com/gitcaddy/gitcaddy-vault $vaultVersion"
|
||||
Set-Content go.mod $content -NoNewline
|
||||
Get-Content go.mod | Select-String "gitcaddy/vault" -Context 0,2
|
||||
go mod tidy
|
||||
go mod download
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -378,7 +387,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies (Unix)
|
||||
if: matrix.goos != 'windows'
|
||||
run: make deps-frontend deps-backend
|
||||
run: |
|
||||
make deps-frontend deps-backend
|
||||
|
||||
- name: Install dependencies (Windows)
|
||||
if: matrix.goos == 'windows'
|
||||
@@ -627,7 +637,7 @@ jobs:
|
||||
echo "Building with vault $VAULT_VERSION"
|
||||
sed -i "s|replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault|replace git.marketally.com/gitcaddy/gitcaddy-vault => git.marketally.com/gitcaddy/gitcaddy-vault $VAULT_VERSION|" go.mod
|
||||
cat go.mod | grep -A2 "gitcaddy-vault" || true
|
||||
/usr/local/go/bin/go mod tidy -v 2>&1
|
||||
/usr/local/go/bin/go mod download
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
10
go.mod
10
go.mod
@@ -2,7 +2,7 @@ module code.gitcaddy.com/server/v3
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.25.5
|
||||
toolchain go1.25.9
|
||||
|
||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||
// But some CAs use negative serial number, just relax the check. related:
|
||||
@@ -326,6 +326,9 @@ replace github.com/go-ini/ini => github.com/go-ini/ini v1.66.6
|
||||
// Use GitCaddy fork with capability support
|
||||
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.8
|
||||
|
||||
// Mirror of deleted github.com/hexops/gotextdiff
|
||||
replace github.com/hexops/gotextdiff => git.marketally.com/mirrors/gotextdiff v1.0.3
|
||||
|
||||
// Vault plugin - use local directory for development
|
||||
replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault
|
||||
|
||||
@@ -337,4 +340,9 @@ exclude github.com/goccy/go-json v0.4.11
|
||||
|
||||
exclude github.com/satori/go.uuid v1.2.0
|
||||
|
||||
// Pin SDK to v0.22.0 — v0.24.x requires go 1.26
|
||||
exclude code.gitea.io/sdk/gitea v0.24.0
|
||||
|
||||
exclude code.gitea.io/sdk/gitea v0.24.1
|
||||
|
||||
tool code.gitea.io/gitea-vet
|
||||
|
||||
6
go.sum
6
go.sum
@@ -31,6 +31,10 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.8 h1:MBipeHvY6A0jcobvziUtzgatZTrV4fs/HE1rPQxREN4=
|
||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.8/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
|
||||
git.marketally.com/gitcaddy/gitcaddy-vault v1.0.60 h1:SHmAZG1PxbC9kG9gWdrhrQ7cHVmP0e6iMloBK100kQA=
|
||||
git.marketally.com/gitcaddy/gitcaddy-vault v1.0.60/go.mod h1:UAmVrJUXHsGe3iJz2gmow2zsIE/KAXaW4jKytrxT8jQ=
|
||||
git.marketally.com/mirrors/gotextdiff v1.0.3 h1:Mxf+YurdCHT4y1GNiZCTDWYtVXSxhlLUeG7g7i9Za70=
|
||||
git.marketally.com/mirrors/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
|
||||
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
||||
@@ -490,8 +494,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
|
||||
@@ -444,6 +444,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(367, "Add pages translation table for multi-language support", v1_26.AddPagesTranslationTable),
|
||||
newMigration(368, "Add owner_display_name to repository", v1_26.AddOwnerDisplayNameToRepository),
|
||||
newMigration(369, "Add public_release_downloads to repository", v1_26.AddPublicReleaseDownloadsToRepository),
|
||||
newMigration(370, "Add exclude_hidden_files to push_mirror", v1_26.AddExcludeHiddenFilesToPushMirror),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
16
models/migrations/v1_26/v370.go
Normal file
16
models/migrations/v1_26/v370.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddExcludeHiddenFilesToPushMirror(x *xorm.Engine) error {
|
||||
type PushMirror struct {
|
||||
ExcludeHiddenFiles bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
return x.Sync(new(PushMirror))
|
||||
}
|
||||
@@ -24,11 +24,12 @@ type PushMirror struct {
|
||||
RemoteName string
|
||||
RemoteAddress string `xorm:"VARCHAR(2048)"`
|
||||
|
||||
SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"`
|
||||
Interval time.Duration
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
|
||||
LastError string `xorm:"text"`
|
||||
SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"`
|
||||
ExcludeHiddenFiles bool `xorm:"NOT NULL DEFAULT false"`
|
||||
Interval time.Duration
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
|
||||
LastError string `xorm:"text"`
|
||||
}
|
||||
|
||||
type PushMirrorOptions struct {
|
||||
|
||||
156
modules/git/tree_filter.go
Normal file
156
modules/git/tree_filter.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitcaddy.com/server/v3/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
// FilterTreeOptions controls which paths are excluded when building a filtered tree.
|
||||
type FilterTreeOptions struct {
|
||||
// ExcludeDotFiles excludes all entries whose name starts with "."
|
||||
ExcludeDotFiles bool
|
||||
// ExcludePaths is a set of specific paths to exclude (exact top-level names)
|
||||
ExcludePaths map[string]bool
|
||||
}
|
||||
|
||||
// ShouldExclude returns true if the given entry name should be filtered out.
|
||||
func (opts *FilterTreeOptions) ShouldExclude(name string) bool {
|
||||
if opts.ExcludeDotFiles && strings.HasPrefix(name, ".") {
|
||||
return true
|
||||
}
|
||||
if opts.ExcludePaths != nil && opts.ExcludePaths[name] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasFilters returns true if any filtering is configured.
|
||||
func (opts *FilterTreeOptions) HasFilters() bool {
|
||||
return opts.ExcludeDotFiles || len(opts.ExcludePaths) > 0
|
||||
}
|
||||
|
||||
// BuildFilteredCommit creates a new commit that has the same content as the
|
||||
// given commit but with excluded paths removed from the top-level tree.
|
||||
// It uses git plumbing (ls-tree, mktree, commit-tree) and creates no refs —
|
||||
// only loose objects that git gc will collect naturally.
|
||||
// Returns the SHA of the new commit, or the original SHA if nothing was filtered.
|
||||
func (repo *Repository) BuildFilteredCommit(commitSHA string, opts *FilterTreeOptions) (string, error) {
|
||||
if !opts.HasFilters() {
|
||||
return commitSHA, nil
|
||||
}
|
||||
|
||||
// 1. Get the top-level tree entries for this commit
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
err := gitcmd.NewCommand("ls-tree").AddArguments("--").AddDynamicArguments(commitSHA).
|
||||
WithDir(repo.Path).
|
||||
WithStdout(stdout).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ls-tree %s: %s: %w", commitSHA, stderr.String(), err)
|
||||
}
|
||||
|
||||
// 2. Filter entries
|
||||
var filteredLines []string
|
||||
excluded := false
|
||||
for line := range strings.SplitSeq(strings.TrimRight(stdout.String(), "\n"), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// ls-tree output format: "<mode> <type> <sha>\t<name>"
|
||||
tabIdx := strings.IndexByte(line, '\t')
|
||||
if tabIdx < 0 {
|
||||
continue
|
||||
}
|
||||
name := line[tabIdx+1:]
|
||||
if opts.ShouldExclude(name) {
|
||||
excluded = true
|
||||
continue
|
||||
}
|
||||
filteredLines = append(filteredLines, line)
|
||||
}
|
||||
|
||||
// Nothing was filtered — return original commit
|
||||
if !excluded {
|
||||
return commitSHA, nil
|
||||
}
|
||||
|
||||
// 3. Create a new tree from filtered entries via mktree
|
||||
treeInput := strings.Join(filteredLines, "\n") + "\n"
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
err = gitcmd.NewCommand("mktree").
|
||||
WithDir(repo.Path).
|
||||
WithStdin(strings.NewReader(treeInput)).
|
||||
WithStdout(stdout).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("mktree: %s: %w", stderr.String(), err)
|
||||
}
|
||||
newTreeSHA := strings.TrimSpace(stdout.String())
|
||||
|
||||
// 4. Get the original commit's parent(s) and message
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
err = gitcmd.NewCommand("log", "-1", "--format=%P%n%B").AddArguments("--").AddDynamicArguments(commitSHA).
|
||||
WithDir(repo.Path).
|
||||
WithStdout(stdout).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("log %s: %s: %w", commitSHA, stderr.String(), err)
|
||||
}
|
||||
|
||||
logOutput := stdout.String()
|
||||
lines := strings.SplitN(logOutput, "\n", 2)
|
||||
parentLine := ""
|
||||
message := "filtered commit"
|
||||
if len(lines) >= 1 {
|
||||
parentLine = strings.TrimSpace(lines[0])
|
||||
}
|
||||
if len(lines) >= 2 {
|
||||
message = strings.TrimRight(lines[1], "\n")
|
||||
}
|
||||
|
||||
// 5. Create a new commit referencing the filtered tree
|
||||
cmd := gitcmd.NewCommand("commit-tree").AddDynamicArguments(newTreeSHA)
|
||||
for parent := range strings.FieldsSeq(parentLine) {
|
||||
cmd = cmd.AddArguments("-p").AddDynamicArguments(parent)
|
||||
}
|
||||
|
||||
messageReader := strings.NewReader(message + "\n")
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
err = cmd.
|
||||
WithDir(repo.Path).
|
||||
WithStdin(messageReader).
|
||||
WithStdout(stdout).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("commit-tree: %s: %w", stderr.String(), err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
// UpdateRef updates a ref to point at the given SHA.
|
||||
func (repo *Repository) UpdateRef(ref, sha string) error {
|
||||
stderr := new(bytes.Buffer)
|
||||
err := gitcmd.NewCommand("update-ref").AddDynamicArguments(ref, sha).
|
||||
WithDir(repo.Path).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update-ref %s %s: %s: %w", ref, sha, stderr.String(), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -30,10 +30,14 @@ type LandingConfig struct {
|
||||
Stats []StatConfig `yaml:"stats,omitempty" json:"stats,omitempty"`
|
||||
|
||||
// Value propositions
|
||||
ValueProps []ValuePropConfig `yaml:"value_props,omitempty" json:"value_props,omitempty"`
|
||||
ValueProps []ValuePropConfig `yaml:"value_props,omitempty" json:"value_props,omitempty"`
|
||||
ValuePropsHeadline string `yaml:"value_props_headline,omitempty" json:"value_props_headline,omitempty"`
|
||||
ValuePropsSubheadline string `yaml:"value_props_subheadline,omitempty" json:"value_props_subheadline,omitempty"`
|
||||
|
||||
// Features
|
||||
Features []FeatureConfig `yaml:"features,omitempty" json:"features,omitempty"`
|
||||
Features []FeatureConfig `yaml:"features,omitempty" json:"features,omitempty"`
|
||||
FeaturesHeadline string `yaml:"features_headline,omitempty" json:"features_headline,omitempty"`
|
||||
FeaturesSubheadline string `yaml:"features_subheadline,omitempty" json:"features_subheadline,omitempty"`
|
||||
|
||||
// Social proof
|
||||
SocialProof SocialProofConfig `yaml:"social_proof,omitempty" json:"social_proof,omitzero"`
|
||||
@@ -53,6 +57,9 @@ type LandingConfig struct {
|
||||
// Comparison section
|
||||
Comparison ComparisonSectionConfig `yaml:"comparison,omitempty" json:"comparison,omitzero"`
|
||||
|
||||
// Cross-promote section
|
||||
CrossPromote CrossPromoteSectionConfig `yaml:"cross_promote,omitempty" json:"cross_promote,omitzero"`
|
||||
|
||||
// Navigation visibility
|
||||
Navigation NavigationConfig `yaml:"navigation,omitempty" json:"navigation,omitzero"`
|
||||
|
||||
@@ -251,6 +258,13 @@ type ComparisonFeatureConfig struct {
|
||||
Values []string `yaml:"values,omitempty" json:"values,omitempty"` // "true"/"false" for check/x, anything else displayed as text
|
||||
}
|
||||
|
||||
// CrossPromoteSectionConfig controls the cross-promote section on the landing page
|
||||
type CrossPromoteSectionConfig struct {
|
||||
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
||||
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
|
||||
}
|
||||
|
||||
// NavigationConfig controls which built-in navigation links appear in the header and footer
|
||||
type NavigationConfig struct {
|
||||
ShowDocs bool `yaml:"show_docs,omitempty" json:"show_docs,omitempty"`
|
||||
@@ -259,16 +273,17 @@ type NavigationConfig struct {
|
||||
ShowReleases bool `yaml:"show_releases,omitempty" json:"show_releases,omitempty"`
|
||||
ShowIssues bool `yaml:"show_issues,omitempty" json:"show_issues,omitempty"`
|
||||
// Translatable labels for nav items and section headers (defaults to English)
|
||||
LabelValueProps string `yaml:"label_value_props,omitempty" json:"label_value_props,omitempty"`
|
||||
LabelFeatures string `yaml:"label_features,omitempty" json:"label_features,omitempty"`
|
||||
LabelPricing string `yaml:"label_pricing,omitempty" json:"label_pricing,omitempty"`
|
||||
LabelBlog string `yaml:"label_blog,omitempty" json:"label_blog,omitempty"`
|
||||
LabelGallery string `yaml:"label_gallery,omitempty" json:"label_gallery,omitempty"`
|
||||
LabelCompare string `yaml:"label_compare,omitempty" json:"label_compare,omitempty"`
|
||||
LabelDocs string `yaml:"label_docs,omitempty" json:"label_docs,omitempty"`
|
||||
LabelReleases string `yaml:"label_releases,omitempty" json:"label_releases,omitempty"`
|
||||
LabelAPI string `yaml:"label_api,omitempty" json:"label_api,omitempty"`
|
||||
LabelIssues string `yaml:"label_issues,omitempty" json:"label_issues,omitempty"`
|
||||
LabelValueProps string `yaml:"label_value_props,omitempty" json:"label_value_props,omitempty"`
|
||||
LabelFeatures string `yaml:"label_features,omitempty" json:"label_features,omitempty"`
|
||||
LabelPricing string `yaml:"label_pricing,omitempty" json:"label_pricing,omitempty"`
|
||||
LabelBlog string `yaml:"label_blog,omitempty" json:"label_blog,omitempty"`
|
||||
LabelGallery string `yaml:"label_gallery,omitempty" json:"label_gallery,omitempty"`
|
||||
LabelCompare string `yaml:"label_compare,omitempty" json:"label_compare,omitempty"`
|
||||
LabelCrossPromote string `yaml:"label_cross_promote,omitempty" json:"label_cross_promote,omitempty"`
|
||||
LabelDocs string `yaml:"label_docs,omitempty" json:"label_docs,omitempty"`
|
||||
LabelReleases string `yaml:"label_releases,omitempty" json:"label_releases,omitempty"`
|
||||
LabelAPI string `yaml:"label_api,omitempty" json:"label_api,omitempty"`
|
||||
LabelIssues string `yaml:"label_issues,omitempty" json:"label_issues,omitempty"`
|
||||
}
|
||||
|
||||
// FooterConfig represents footer settings
|
||||
@@ -477,58 +492,64 @@ func TemplateDefaultLabels(template string) NavigationConfig {
|
||||
switch template {
|
||||
case "architecture-deep-dive":
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Systems Analysis",
|
||||
LabelFeatures: "Technical Specifications",
|
||||
LabelPricing: "Resource Allocation",
|
||||
LabelBlog: "Dispatches",
|
||||
LabelGallery: "Visual Index",
|
||||
LabelCompare: "Compare",
|
||||
LabelValueProps: "Systems Analysis",
|
||||
LabelFeatures: "Technical Specifications",
|
||||
LabelPricing: "Resource Allocation",
|
||||
LabelBlog: "Dispatches",
|
||||
LabelGallery: "Visual Index",
|
||||
LabelCompare: "Compare",
|
||||
LabelCrossPromote: "Related Offerings",
|
||||
}
|
||||
case "bold-marketing":
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Why choose this",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Investment",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
LabelValueProps: "Why choose this",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Investment",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
LabelCrossPromote: "Related Offerings",
|
||||
}
|
||||
case "minimalist-docs":
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Why choose this",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Investment",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
LabelValueProps: "Why choose this",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Investment",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
LabelCrossPromote: "Related Offerings",
|
||||
}
|
||||
case "open-source-hero":
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Why choose us",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Pricing",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
LabelValueProps: "Why choose us",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Pricing",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
LabelCrossPromote: "Related Offerings",
|
||||
}
|
||||
case "saas-conversion":
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Why",
|
||||
LabelFeatures: "Features",
|
||||
LabelPricing: "Pricing",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
LabelValueProps: "Why",
|
||||
LabelFeatures: "Features",
|
||||
LabelPricing: "Pricing",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
LabelCrossPromote: "Related Offerings",
|
||||
}
|
||||
default:
|
||||
// developer-tool, documentation-first, visual-showcase, cli-terminal
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Why choose us",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Pricing",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
LabelValueProps: "Why choose us",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Pricing",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
LabelCrossPromote: "Related Offerings",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,3 +191,25 @@ type OrgProfileContent struct {
|
||||
Readme string `json:"readme,omitempty"`
|
||||
HasCSS bool `json:"has_css"`
|
||||
}
|
||||
|
||||
// OrgRecentActivity represents a recently updated repo in the org overview
|
||||
type OrgRecentActivity struct {
|
||||
RepoName string `json:"repo_name"`
|
||||
RepoFullName string `json:"repo_full_name"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
CommitMessage string `json:"commit_message,omitempty"`
|
||||
CommitTime int64 `json:"commit_time,omitempty"`
|
||||
IsPrivate bool `json:"is_private"`
|
||||
}
|
||||
|
||||
// OrgOverviewV2 represents the enhanced organization overview for v2 API
|
||||
type OrgOverviewV2 struct {
|
||||
Organization *Organization `json:"organization"`
|
||||
PinnedRepos []*OrgPinnedRepo `json:"pinned_repos"`
|
||||
PinnedGroups []*OrgPinnedGroup `json:"pinned_groups"`
|
||||
PublicMembers []*OrgPublicMember `json:"public_members"`
|
||||
TotalMembers int64 `json:"total_members"`
|
||||
Stats *OrgOverviewStats `json:"stats"`
|
||||
Profile *OrgProfileContent `json:"profile,omitempty"`
|
||||
RecentActivity []OrgRecentActivity `json:"recent_activity,omitempty"`
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2303,6 +2303,8 @@
|
||||
"repo.settings.mirror_settings.push_mirror.none": "No push mirrors configured",
|
||||
"repo.settings.mirror_settings.push_mirror.remote_url": "Git Remote Repository URL",
|
||||
"repo.settings.mirror_settings.push_mirror.add": "Add Push Mirror",
|
||||
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files": "Exclude hidden files and folders",
|
||||
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc": "Do not push files/folders starting with \".\" or marked as hidden to the remote mirror",
|
||||
"repo.settings.mirror_settings.push_mirror.edit_sync_time": "Edit mirror sync interval",
|
||||
"repo.settings.sync_mirror": "Synchronize Now",
|
||||
"repo.settings.pull_mirror_sync_in_progress": "Pulling changes from the remote %s at the moment.",
|
||||
@@ -4503,6 +4505,12 @@
|
||||
"repo.settings.pages.app_store_id_desc": "App ID from the App Store URL (e.g. id123456789)",
|
||||
"repo.settings.pages.stats": "Stats",
|
||||
"repo.settings.pages.value_props": "Value Propositions",
|
||||
"repo.settings.pages.value_props_headline": "Section Headline",
|
||||
"repo.settings.pages.value_props_headline_help": "Large heading for the value propositions section (e.g., \"Built for makers\"). The small label above it is set in Section Labels.",
|
||||
"repo.settings.pages.value_props_subheadline": "Section Subheadline",
|
||||
"repo.settings.pages.features_headline": "Section Headline",
|
||||
"repo.settings.pages.features_headline_help": "Large heading for the features section (e.g., \"Everything you need\"). The small label above it is set in Section Labels.",
|
||||
"repo.settings.pages.features_subheadline": "Section Subheadline",
|
||||
"repo.settings.pages.features": "Features",
|
||||
"repo.settings.pages.company_logos": "Company Logos",
|
||||
"repo.settings.pages.testimonials": "Testimonials",
|
||||
@@ -4545,6 +4553,12 @@
|
||||
"repo.settings.pages.nav_show_repository": "Show Repository link (View Source button)",
|
||||
"repo.settings.pages.nav_show_releases": "Show Releases link",
|
||||
"repo.settings.pages.nav_show_issues": "Show Issues link",
|
||||
"repo.settings.pages.section_labels": "Section Labels",
|
||||
"repo.settings.pages.section_labels_desc": "Customize the headings shown on your landing page for each section",
|
||||
"repo.settings.pages.label_value_props": "Value Props Heading",
|
||||
"repo.settings.pages.label_value_props_help": "Heading for the value propositions section (e.g., \"Why choose us\")",
|
||||
"repo.settings.pages.label_features": "Features Heading",
|
||||
"repo.settings.pages.label_features_help": "Heading for the features section (e.g., \"Capabilities\")",
|
||||
"repo.settings.pages.blog_section": "Blog Section",
|
||||
"repo.settings.pages.blog_enabled_desc": "Show recent blog posts on the landing page",
|
||||
"repo.settings.pages.blog_headline": "Blog Headline",
|
||||
@@ -4558,6 +4572,11 @@
|
||||
"repo.settings.pages.gallery_columns": "Grid Columns",
|
||||
"repo.settings.pages.gallery_help_link": "Upload and manage gallery images in Settings > Gallery.",
|
||||
"repo.settings.pages.comparison": "Comparison",
|
||||
"repo.settings.pages.cross_promote_section": "Cross-Promote Section",
|
||||
"repo.settings.pages.cross_promote_enabled_desc": "Show cross-promoted repositories on the landing page (only repos with landing pages enabled are shown)",
|
||||
"repo.settings.pages.cross_promote_headline": "Section Headline",
|
||||
"repo.settings.pages.cross_promote_subheadline": "Section Subheadline",
|
||||
"repo.settings.pages.cross_promote_help": "Configure which repositories to cross-promote in Settings > Cross-Promote. Only repos with landing pages enabled will appear.",
|
||||
"repo.settings.pages.comparison_section": "Comparison Section",
|
||||
"repo.settings.pages.comparison_enabled_desc": "Show a feature comparison table on the landing page",
|
||||
"repo.settings.pages.comparison_headline": "Headline",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,8 +15,8 @@
|
||||
"template": "Template",
|
||||
"language": "Bahasa",
|
||||
"notifications": "Notifikasi",
|
||||
"create_new": "Buat…",
|
||||
"user_profile_and_more": "Profil dan Pengaturan…",
|
||||
"create_new": "Buat\u00e2\u20ac\u00a6",
|
||||
"user_profile_and_more": "Profil dan Pengaturan\u00e2\u20ac\u00a6",
|
||||
"signed_in_as": "Masuk sebagai",
|
||||
"toc": "Daftar Isi",
|
||||
"username": "Nama Pengguna",
|
||||
@@ -28,7 +28,7 @@
|
||||
"passcode": "Kode Akses",
|
||||
"webauthn_insert_key": "Masukkan kunci keamanan anda",
|
||||
"webauthn_sign_in": "Tekan tombol pada kunci keamanan Anda. Jika kunci keamanan Anda tidak memiliki tombol, masukkan kembali.",
|
||||
"webauthn_press_button": "Silakan tekan tombol pada kunci keamanan Anda…",
|
||||
"webauthn_press_button": "Silakan tekan tombol pada kunci keamanan Anda\u00e2\u20ac\u00a6",
|
||||
"webauthn_use_twofa": "Gunakan kode dua faktor dari telepon Anda",
|
||||
"webauthn_error": "Tidak dapat membaca kunci keamanan Anda.",
|
||||
"webauthn_unsupported_browser": "Browser Anda saat ini tidak mendukung WebAuthn.",
|
||||
@@ -90,7 +90,7 @@
|
||||
"copy_type_unsupported": "Tipe berkas ini tidak dapat disalin",
|
||||
"write": "Tulis",
|
||||
"preview": "Pratinjau",
|
||||
"loading": "Memuat…",
|
||||
"loading": "Memuat\u00e2\u20ac\u00a6",
|
||||
"error_title": "Gangguan",
|
||||
"error404": "Halaman yang akan kamu akses <strong>tidak dapat ditemukan</strong> atau <strong>kamu tidak memiliki akses </strong> untuk melihatnya.",
|
||||
"go_back": "Kembali",
|
||||
@@ -175,7 +175,7 @@
|
||||
"home.password_holder": "Kata Sandi",
|
||||
"home.switch_dashboard_context": "Alihkan Dasbor Konteks",
|
||||
"home.my_repos": "Repositori",
|
||||
"home.show_more_repos": "Tampilkan repositori lainnya…",
|
||||
"home.show_more_repos": "Tampilkan repositori lainnya\u00e2\u20ac\u00a6",
|
||||
"home.collaborative_repos": "Repositori Kolaboratif",
|
||||
"home.my_orgs": "Organisasi Saya",
|
||||
"home.my_mirrors": "Duplikat Saya",
|
||||
@@ -295,7 +295,7 @@
|
||||
"form.invalid_gpg_key": "Tidak dapat memverifikasi kunci GPG Anda: %s",
|
||||
"form.auth_failed": "Otentikasi gagal: %v",
|
||||
"form.target_branch_not_exist": "Target cabang tidak ada.",
|
||||
"user.change_avatar": "Ganti avatar anda…",
|
||||
"user.change_avatar": "Ganti avatar anda\u00e2\u20ac\u00a6",
|
||||
"user.repositories": "Repositori",
|
||||
"user.activity": "Aktivitas Publik",
|
||||
"user.followers": "Pengikut",
|
||||
@@ -564,18 +564,18 @@
|
||||
"repo.editor.fork_before_edit": "Anda harus mencabangkan repositori ini untuk membuat atau mengusulkan perubahan pada berkas ini.",
|
||||
"repo.editor.delete_this_file": "Hapus Berkas",
|
||||
"repo.editor.must_have_write_access": "Anda harus punya akses tulis untuk membuat atau mengusulkan perubahan pada berkas ini.",
|
||||
"repo.editor.name_your_file": "Nama berkas…",
|
||||
"repo.editor.name_your_file": "Nama berkas\u00e2\u20ac\u00a6",
|
||||
"repo.editor.filename_help": "Tambahkan direktori dengan mengetikkan nama direktori diikuti dengan garis miring ('/'). Hapus direktori dengan mengetikkan spasi balik pada awal bidang input.",
|
||||
"repo.editor.or": "atau",
|
||||
"repo.editor.cancel_lower": "Batalkan",
|
||||
"repo.editor.commit_changes": "Perubahan komitmen",
|
||||
"repo.editor.commit_message": "Commit message",
|
||||
"repo.editor.commit_message_desc": "Tambahkan deskripsi opsional yang panjang…",
|
||||
"repo.editor.commit_message_desc": "Tambahkan deskripsi opsional yang panjang\u00e2\u20ac\u00a6",
|
||||
"repo.editor.commit_directly_to_this_branch": "Komitmen langsung ke <strong class=\"branch-name\">%s</strong> cabang.",
|
||||
"repo.editor.create_new_branch": "Membuat <strong>new branch</strong> untuk tarik komit ini mulai permintaan.",
|
||||
"repo.editor.create_new_branch_np": "Buat <strong>cabang baru</strong> untuk komit ini.",
|
||||
"repo.editor.propose_file_change": "Usul perubahan berkas",
|
||||
"repo.editor.new_branch_name_desc": "Nama branch baru…",
|
||||
"repo.editor.new_branch_name_desc": "Nama branch baru\u00e2\u20ac\u00a6",
|
||||
"repo.editor.cancel": "Membatalkan",
|
||||
"repo.editor.no_changes_to_show": "Tidak ada perubahan untuk ditampilkan.",
|
||||
"repo.commits.commits": "Melakukan",
|
||||
@@ -816,7 +816,7 @@
|
||||
"repo.settings.title": "Judul",
|
||||
"repo.settings.deploy_key_content": "Konten",
|
||||
"repo.settings.branches": "Cabang",
|
||||
"repo.settings.choose_branch": "Pilih branch…",
|
||||
"repo.settings.choose_branch": "Pilih branch\u00e2\u20ac\u00a6",
|
||||
"repo.settings.tags": "Tag",
|
||||
"repo.diff.browse_source": "Telusuri Sumber",
|
||||
"repo.diff.parent": "orang tua",
|
||||
@@ -1315,24 +1315,24 @@
|
||||
"search.search": "Cari",
|
||||
"search.fuzzy_tooltip": "Include results that closely match the search term",
|
||||
"search.regexp": "Regexp",
|
||||
"search.repo_kind": "Search repos…",
|
||||
"search.user_kind": "Search users…",
|
||||
"search.org_kind": "Search orgs…",
|
||||
"search.team_kind": "Search teams…",
|
||||
"search.code_kind": "Search code…",
|
||||
"search.package_kind": "Search packages…",
|
||||
"search.project_kind": "Search projects…",
|
||||
"search.branch_kind": "Search branches…",
|
||||
"search.tag_kind": "Search tags…",
|
||||
"search.commit_kind": "Search commits…",
|
||||
"search.runner_kind": "Search runners…",
|
||||
"search.issue_kind": "Search issues…",
|
||||
"search.pull_kind": "Search pull requests…",
|
||||
"search.repo_kind": "Search repos\u2026",
|
||||
"search.user_kind": "Search users\u2026",
|
||||
"search.org_kind": "Search orgs\u2026",
|
||||
"search.team_kind": "Search teams\u2026",
|
||||
"search.code_kind": "Search code\u2026",
|
||||
"search.package_kind": "Search packages\u2026",
|
||||
"search.project_kind": "Search projects\u2026",
|
||||
"search.branch_kind": "Search branches\u2026",
|
||||
"search.tag_kind": "Search tags\u2026",
|
||||
"search.commit_kind": "Search commits\u2026",
|
||||
"search.runner_kind": "Search runners\u2026",
|
||||
"search.issue_kind": "Search issues\u2026",
|
||||
"search.pull_kind": "Search pull requests\u2026",
|
||||
"editor.buttons.strikethrough.tooltip": "Add strikethrough text",
|
||||
"filter.string.asc": "A–Z",
|
||||
"filter.string.desc": "Z–A",
|
||||
"filter.string.asc": "A\u2013Z",
|
||||
"filter.string.desc": "Z\u2013A",
|
||||
"install.install": "Installation",
|
||||
"install.installing_desc": "Installing now, please wait…",
|
||||
"install.installing_desc": "Installing now, please wait\u2026",
|
||||
"install.host": "Host",
|
||||
"install.db_schema": "Schema",
|
||||
"install.ssl_mode": "SSL",
|
||||
@@ -1409,7 +1409,7 @@
|
||||
"repo.readme": "README",
|
||||
"repo.mirror_address_url_invalid": "The provided URL is invalid. Make sure all components of the URL are escaped correctly.",
|
||||
"repo.mirror_lfs_endpoint_desc": "Sync will attempt to use the clone URL to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">determine the LFS server</a>. You can also specify a custom endpoint if the repository LFS data is stored somewhere else.",
|
||||
"repo.adopt_search": "Enter username to search for unadopted repositories… (leave blank to find all)",
|
||||
"repo.adopt_search": "Enter username to search for unadopted repositories\u2026 (leave blank to find all)",
|
||||
"repo.desc.sha256": "SHA256",
|
||||
"repo.template.webhooks": "Webhooks",
|
||||
"repo.archive.title": "This repo is archived. You can view files and clone it. You cannot open issues or pull requests or push a commit.",
|
||||
@@ -1419,7 +1419,7 @@
|
||||
"repo.migrate_items_labels": "Labels",
|
||||
"repo.migrate.github_token_desc": "You can put one or more tokens here, separated by commas, to make migrating faster by circumventing the GitHub API rate limit. WARNING: Abusing this feature may violate the service provider's policy and may lead to getting your account(s) blocked.",
|
||||
"repo.migrate.permission_denied_blocked": "You cannot import from disallowed hosts. Please ask the admin to check ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS settings.",
|
||||
"repo.migrate.migrating": "Migrating from <b>%s</b>…",
|
||||
"repo.migrate.migrating": "Migrating from <b>%s</b>\u2026",
|
||||
"repo.migrate.codecommit.aws_access_key_id": "AWS Access Key ID",
|
||||
"repo.migrate.codecommit.aws_secret_access_key": "AWS Secret Access Key",
|
||||
"repo.migration_status": "Migration status",
|
||||
@@ -1468,13 +1468,13 @@
|
||||
"repo.issues.review.review": "Review",
|
||||
"repo.issues.assignee.error": "Not all assignees were added, due to an unexpected error.",
|
||||
"repo.compare.title": "Comparing changes",
|
||||
"repo.compare.description": "Choose two branches or tags to see what’s changed or to start a new pull request.",
|
||||
"repo.compare.description": "Choose two branches or tags to see what\u2019s changed or to start a new pull request.",
|
||||
"repo.pulls.new.description": "Discuss and review the changes in this comparison with others.",
|
||||
"repo.pulls.new.already_existed": "A pull request between these branches already exists",
|
||||
"repo.pulls.edit.already_changed": "Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.",
|
||||
"repo.pulls.select_commit_hold_shift_for_range": "Select commit. Hold Shift and click to select a range.",
|
||||
"repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.",
|
||||
"repo.pulls.is_checking": "Checking for merge conflicts…",
|
||||
"repo.pulls.is_checking": "Checking for merge conflicts\u2026",
|
||||
"repo.pulls.wrong_commit_id": "commit ID must be a commit ID on the target branch",
|
||||
"repo.pulls.no_merge_not_ready": "This pull request is not ready to be merged. Check review status and status checks.",
|
||||
"repo.pulls.rebase_merge_pull_request": "Rebase, then fast-forward",
|
||||
@@ -1492,7 +1492,7 @@
|
||||
"repo.pulls.status_checks_need_approvals_helper": "The workflow will only run after approval from the repository maintainer.",
|
||||
"repo.pulls.cmd_instruction_checkout_title": "Checkout",
|
||||
"repo.pulls.cmd_instruction_merge_warning": "Warning: This operation cannot merge pull request because \"autodetect manual merge\" is not enabled.",
|
||||
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By…\".",
|
||||
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By\u2026\".",
|
||||
"repo.signing.wont_sign.error": "There was an error while checking if the commit could be signed.",
|
||||
"repo.signing.wont_sign.twofa": "You must have two-factor authentication enabled to have commits signed.",
|
||||
"repo.wiki": "Wiki",
|
||||
@@ -1548,8 +1548,8 @@
|
||||
"repo.settings.visibility.private.bullet_two": "May remove the relationship between it and <strong>forks</strong>, <strong>watchers</strong>, and <strong>stars</strong>.",
|
||||
"repo.settings.unarchive.text": "Unarchiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull requests.",
|
||||
"repo.settings.lfs": "LFS",
|
||||
"repo.settings.lfs_lock_path": "Filepath to lock…",
|
||||
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) — %d associated, %d unassociated (%d missing from store)",
|
||||
"repo.settings.lfs_lock_path": "Filepath to lock\u2026",
|
||||
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) \u2014 %d associated, %d unassociated (%d missing from store)",
|
||||
"repo.settings.lfs_pointers.sha": "Blob SHA",
|
||||
"repo.settings.lfs_pointers.oid": "OID",
|
||||
"repo.settings.pages": "Landing Page",
|
||||
@@ -1607,7 +1607,7 @@
|
||||
"repo.branch.commits_divergence_from": "Commit divergence: %[1]d behind and %[2]d ahead of %[3]s",
|
||||
"repo.branch.commits_no_divergence": "The same as branch %[1]s",
|
||||
"repo.find_file.follow_symlink": "Follow this symlink to where it is pointing at",
|
||||
"graphs.component_loading": "Loading %s…",
|
||||
"graphs.component_loading": "Loading %s\u2026",
|
||||
"org.pinned_repos": "Featured Projects",
|
||||
"org.public_members": "Public Members",
|
||||
"org.view_all_members": "View all %d members",
|
||||
@@ -1615,7 +1615,7 @@
|
||||
"org.settings.email": "Contact Email Address",
|
||||
"org.settings.change_visibility": "Change Visibility",
|
||||
"org.settings.change_visibility_notices_1": "If the organization is converted to private, the repository stars will be removed and cannot be restored.",
|
||||
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organization’s repositories if visibility is changed to private.",
|
||||
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organization\u2019s repositories if visibility is changed to private.",
|
||||
"org.settings.change_visibility_success": "The visibility of organization %s has been successfully changed.",
|
||||
"org.settings.visibility_desc": "Change who can view the organization and its repositories.",
|
||||
"org.settings.rename": "Rename Organization",
|
||||
@@ -1670,11 +1670,11 @@
|
||||
"admin.auths.port": "Port",
|
||||
"admin.auths.ssh_keys_are_verified": "SSH keys in LDAP are considered as verified",
|
||||
"admin.auths.helo_hostname": "HELO Hostname",
|
||||
"admin.auths.oauth2_full_name_claim_name": "Full Name Claim Name. (Optional — if set, the user's full name will always be synchronized with this claim)",
|
||||
"admin.auths.oauth2_full_name_claim_name": "Full Name Claim Name. (Optional \u2014 if set, the user's full name will always be synchronized with this claim)",
|
||||
"admin.auths.oauth2_ssh_public_key_claim_name": "SSH Public Key Claim Name",
|
||||
"admin.auths.oauth2_admin_group": "Group Claim value for administrator users. (Optional — requires claim name above)",
|
||||
"admin.auths.oauth2_restricted_group": "Group Claim value for restricted users. (Optional — requires claim name above)",
|
||||
"admin.auths.oauth2_map_group_to_team": "Map claimed groups to Organization teams. (Optional — requires claim name above)",
|
||||
"admin.auths.oauth2_admin_group": "Group Claim value for administrator users. (Optional \u2014 requires claim name above)",
|
||||
"admin.auths.oauth2_restricted_group": "Group Claim value for restricted users. (Optional \u2014 requires claim name above)",
|
||||
"admin.auths.oauth2_map_group_to_team": "Map claimed groups to Organization teams. (Optional \u2014 requires claim name above)",
|
||||
"admin.auths.sspi_auto_create_users_helper": "Allow SSPI auth method to automatically create new accounts for users that log in for the first time",
|
||||
"admin.auths.sspi_strip_domain_names_helper": "If checked, domain names will be removed from logon names (e.g. \"DOMAIN\\user\" and \"user@example.org\" both will become just \"user\").",
|
||||
"admin.auths.sspi_separator_replacement_helper": "The character to use to replace the separators of down-level logon names (e.g. the \\ in \"DOMAIN\\user\") and user principal names (e.g. the @ in \"user@example.org\").",
|
||||
@@ -1784,7 +1784,7 @@
|
||||
"actions.general.collaborative_owner_not_exist": "The collaborative owner does not exist.",
|
||||
"actions.general.remove_collaborative_owner": "Remove Collaborative Owner",
|
||||
"actions.general.remove_collaborative_owner_desc": "Removing a collaborative owner will prevent the repositories of the owner from accessing the actions in this repository. Continue?",
|
||||
"git.filemode.changed_filemode": "%[1]s → %[2]s",
|
||||
"git.filemode.changed_filemode": "%[1]s \u2192 %[2]s",
|
||||
"org.pinned_repos_empty_title": "Showcase your best work",
|
||||
"org.pinned_repos_empty_desc": "Pin up to 6 repositories to highlight your organization's most important projects.",
|
||||
"org.settings.pinned.manage": "Manage Pins",
|
||||
@@ -1954,6 +1954,12 @@
|
||||
"repo.settings.pages.nav_show_repository": "Tampilkan tautan repositori (tombol Lihat kode sumber)",
|
||||
"repo.settings.pages.nav_show_releases": "Tampilkan tautan rilis",
|
||||
"repo.settings.pages.nav_show_issues": "Tampilkan tautan isu",
|
||||
"repo.settings.pages.section_labels": "Label bagian",
|
||||
"repo.settings.pages.section_labels_desc": "Sesuaikan judul yang ditampilkan di halaman arahan untuk setiap bagian",
|
||||
"repo.settings.pages.label_value_props": "Judul proposisi nilai",
|
||||
"repo.settings.pages.label_value_props_help": "Judul untuk bagian proposisi nilai (mis., \"Mengapa memilih kami\")",
|
||||
"repo.settings.pages.label_features": "Judul fitur",
|
||||
"repo.settings.pages.label_features_help": "Judul untuk bagian fitur (mis., \"Kemampuan\")",
|
||||
"repo.settings.pages.blog_section": "Bagian blog",
|
||||
"repo.settings.pages.blog_enabled_desc": "Tampilkan postingan blog terbaru di halaman arahan",
|
||||
"repo.settings.pages.blog_headline": "Judul blog",
|
||||
@@ -2311,5 +2317,18 @@
|
||||
"repo.view_file": "View File",
|
||||
"actions.runners.waiting_jobs": "Pekerjaan Menunggu",
|
||||
"actions.runners.back_to_runners": "Kembali ke Runner",
|
||||
"actions.runners.no_waiting_jobs": "Tidak ada pekerjaan yang menunggu untuk label ini"
|
||||
}
|
||||
"actions.runners.no_waiting_jobs": "Tidak ada pekerjaan yang menunggu untuk label ini",
|
||||
"repo.settings.pages.cross_promote_section": "Bagian promosi silang",
|
||||
"repo.settings.pages.cross_promote_enabled_desc": "Tampilkan repositori promosi silang di halaman arahan",
|
||||
"repo.settings.pages.cross_promote_headline": "Judul bagian",
|
||||
"repo.settings.pages.cross_promote_subheadline": "Subjudul bagian",
|
||||
"repo.settings.pages.cross_promote_help": "Konfigurasikan repositori di Pengaturan > Promosi Silang.",
|
||||
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files": "Kecualikan file dan folder tersembunyi",
|
||||
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc": "Jangan dorong file/folder yang dimulai dengan \".\" atau ditandai sebagai tersembunyi ke mirror jarak jauh",
|
||||
"repo.settings.pages.value_props_headline": "Judul bagian",
|
||||
"repo.settings.pages.value_props_headline_help": "Judul besar untuk bagian proposisi nilai. Label kecil di atasnya diatur di Label bagian.",
|
||||
"repo.settings.pages.value_props_subheadline": "Subjudul bagian",
|
||||
"repo.settings.pages.features_headline": "Judul bagian",
|
||||
"repo.settings.pages.features_headline_help": "Judul besar untuk bagian fitur. Label kecil di atasnya diatur di Label bagian.",
|
||||
"repo.settings.pages.features_subheadline": "Subjudul bagian"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,8 @@
|
||||
"language": "Taal",
|
||||
"notifications": "Meldingen",
|
||||
"active_stopwatch": "Actieve Tijd Tracker",
|
||||
"create_new": "Maken…",
|
||||
"user_profile_and_more": "Profiel en instellingen…",
|
||||
"create_new": "Maken\u00e2\u20ac\u00a6",
|
||||
"user_profile_and_more": "Profiel en instellingen\u00e2\u20ac\u00a6",
|
||||
"signed_in_as": "Aangemeld als",
|
||||
"toc": "Inhoudsopgave",
|
||||
"licenses": "Licenties",
|
||||
@@ -30,7 +30,7 @@
|
||||
"passcode": "PIN",
|
||||
"webauthn_insert_key": "Voer uw beveiligingssleutel in",
|
||||
"webauthn_sign_in": "Druk op de knop van uw beveiligingssleutel. Als uw beveiligingssleutel geen knop heeft, voeg deze dan opnieuw in.",
|
||||
"webauthn_press_button": "Druk alstublieft op de knop van uw beveiligingssleutel…",
|
||||
"webauthn_press_button": "Druk alstublieft op de knop van uw beveiligingssleutel\u00e2\u20ac\u00a6",
|
||||
"webauthn_use_twofa": "Gebruik een twee-factor code van uw telefoon",
|
||||
"webauthn_error": "Kon uw beveiligingssleutel niet lezen.",
|
||||
"webauthn_unsupported_browser": "Uw browser ondersteunt momenteel geen WebAuthn.",
|
||||
@@ -73,11 +73,11 @@
|
||||
"edit": "Bewerken",
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"copy": "Kopiëren",
|
||||
"copy": "Kopi\u00ebren",
|
||||
"copy_url": "Kopieer URL",
|
||||
"copy_branch": "Kopieer branchnaam",
|
||||
"copy_success": "Gekopieerd!",
|
||||
"copy_error": "Kopiëren mislukt",
|
||||
"copy_error": "Kopi\u00c3\u00abren mislukt",
|
||||
"write": "Schrijven",
|
||||
"preview": "Voorbeeld",
|
||||
"loading": "Laden...",
|
||||
@@ -96,12 +96,12 @@
|
||||
"error.occurred": "Er is een fout opgetreden",
|
||||
"error.not_found": "Het doel kon niet worden gevonden.",
|
||||
"error.network_error": "Netwerk fout",
|
||||
"startpage.app_desc": "Geïntegreerd in je workflow",
|
||||
"startpage.app_desc": "Ge\u00c3\u00afntegreerd in je workflow",
|
||||
"startpage.install": "Overal Implementeren",
|
||||
"startpage.lightweight": "Razendsnel",
|
||||
"startpage.lightweight_desc": "Minimale voetafdruk, maximale prestaties. GitCaddy draait efficiënt op alles, van Raspberry Pi tot enterprise servers.",
|
||||
"startpage.lightweight_desc": "Minimale voetafdruk, maximale prestaties. GitCaddy draait effici\u00c3\u00abnt op alles, van Raspberry Pi tot enterprise servers.",
|
||||
"install.install": "Installatie",
|
||||
"install.title": "Initiële configuratie",
|
||||
"install.title": "Initi\u00c3\u00able configuratie",
|
||||
"install.docker_helper": "Als je GitCaddy draait in Docker, Lees eerst de <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">documentatie</a> voordat je een instelling aanpast.",
|
||||
"install.require_db_desc": "GitCaddy vereist MySQL, PostgreSQL, MSSQL, SQLite3 of TiDB (MySQL protocol).",
|
||||
"install.db_title": "Database-instellingen",
|
||||
@@ -162,7 +162,7 @@
|
||||
"install.enable_captcha": "Registratie CAPTCHA inschakelen",
|
||||
"install.enable_captcha_popup": "Vereis captcha validatie voor zelf-registratie van gebruiker.",
|
||||
"install.require_sign_in_view": "Vereis inloggen om pagina's te kunnen bekijken",
|
||||
"install.admin_setting_desc": "Het creëren van een administrator-account is optioneel. De eerste geregistreerde gebruiker wordt automatisch de beheerder.",
|
||||
"install.admin_setting_desc": "Het cre\u00c3\u00abren van een administrator-account is optioneel. De eerste geregistreerde gebruiker wordt automatisch de beheerder.",
|
||||
"install.admin_title": "Instellingen beheerdersaccount",
|
||||
"install.admin_name": "Admin gebruikersnaam",
|
||||
"install.admin_password": "Wachtwoord",
|
||||
@@ -170,7 +170,7 @@
|
||||
"install.admin_email": "E-mailadres",
|
||||
"install.install_btn_confirm": "Installeer GitCaddy",
|
||||
"install.test_git_failed": "Git test niet gelukt: 'git' commando %v",
|
||||
"install.sqlite3_not_available": "Deze GitCaddy-versie biedt geen ondersteuning voor SQLite3. Download de officiële build van %s (niet de versie van de 'gobuild').",
|
||||
"install.sqlite3_not_available": "Deze GitCaddy-versie biedt geen ondersteuning voor SQLite3. Download de offici\u00c3\u00able build van %s (niet de versie van de 'gobuild').",
|
||||
"install.invalid_db_setting": "De database instelling zijn niet correct: %v",
|
||||
"install.invalid_repo_path": "Het pad van de hoofdmap van de repository is ongeldig: %v",
|
||||
"install.invalid_app_data_path": "Ongeldig app-gegevenspad: %v",
|
||||
@@ -192,7 +192,7 @@
|
||||
"home.uname_holder": "Gebruikersnaam of e-mailadres",
|
||||
"home.password_holder": "Wachtwoord",
|
||||
"home.switch_dashboard_context": "Wissel voorpaginacontext",
|
||||
"home.show_more_repos": "Toon meer repositories…",
|
||||
"home.show_more_repos": "Toon meer repositories\u00e2\u20ac\u00a6",
|
||||
"home.collaborative_repos": "Gedeelde repositories",
|
||||
"home.my_orgs": "Mijn organisaties",
|
||||
"home.my_mirrors": "Mijn spiegels",
|
||||
@@ -204,16 +204,16 @@
|
||||
"home.show_both_archived_unarchived": "Toont zowel gearchiveerd als niet-gearchiveerd",
|
||||
"home.show_only_archived": "Toon alleen gearchiveerd",
|
||||
"home.show_only_unarchived": "Toon alleen niet gearchiveerd",
|
||||
"home.show_private": "Privé",
|
||||
"home.show_both_private_public": "Toon zowel openbaar als privé",
|
||||
"home.show_only_private": "Toon alleen privé",
|
||||
"home.show_private": "Priv\u00c3\u00a9",
|
||||
"home.show_both_private_public": "Toon zowel openbaar als priv\u00c3\u00a9",
|
||||
"home.show_only_private": "Toon alleen priv\u00c3\u00a9",
|
||||
"home.show_only_public": "Toon alleen opbenbaar",
|
||||
"home.issues.in_your_repos": "In uw repositories",
|
||||
"explore.users": "Gebruikers",
|
||||
"explore.organizations": "Organisaties",
|
||||
"explore.packages": "Pakketten",
|
||||
"explore.packages.empty.description": "Er zijn nog geen openbare of globale pakketten beschikbaar.",
|
||||
"explore.code_last_indexed_at": "Laatst geïndexeerd %s",
|
||||
"explore.code_last_indexed_at": "Laatst ge\u00c3\u00afndexeerd %s",
|
||||
"auth.create_new_account": "Account registreren",
|
||||
"auth.disable_register_prompt": "Registratie is uitgeschakeld. Neem alstublieft contact op met de pagina beheerder.",
|
||||
"auth.disable_register_mail": "E-mailbevestiging voor registratie is uitgeschakeld.",
|
||||
@@ -236,7 +236,7 @@
|
||||
"auth.reset_password_helper": "Account herstellen",
|
||||
"auth.password_too_short": "De lengte van uw wachtwoord moet tenminste %d karakters zijn.",
|
||||
"auth.non_local_account": "Non-lokale gebruikers mogen hun wachtwoord niet wijzigen via de webinterface.",
|
||||
"auth.verify": "Verifiëren",
|
||||
"auth.verify": "Verifi\u00c3\u00abren",
|
||||
"auth.scratch_code": "Eenmalige code",
|
||||
"auth.use_scratch_code": "Gebruik een eenmalige code",
|
||||
"auth.twofa_scratch_used": "Je hebt je eenmalige code gebruikt. Je wordt omgeleid naar de tweeledige-authenticatie instellingen pagina zodat je de inschrijving van het apparaat kan verwijderen of een nieuwe eenmalige code kan genereren.",
|
||||
@@ -265,7 +265,7 @@
|
||||
"auth.sspi_auth_failed": "SSPI-authenticatie mislukt",
|
||||
"auth.password_pwned_err": "Kan het verzoek om HaveIBeenPwned niet voltooien",
|
||||
"mail.view_it_on": "Bekijk het op %s",
|
||||
"mail.link_not_working_do_paste": "Werkt dit niet? Probeer het te kopiëren en te plakken naar uw browser.",
|
||||
"mail.link_not_working_do_paste": "Werkt dit niet? Probeer het te kopi\u00c3\u00abren en te plakken naar uw browser.",
|
||||
"mail.hi_user_x": "Hoi <b>%s</b>,",
|
||||
"mail.activate_account": "Activeer uw account",
|
||||
"mail.activate_account.title": "%s, activeer alstublieft uw account",
|
||||
@@ -344,36 +344,36 @@
|
||||
"form.username_been_taken": "Deze naam is al in gebruik.",
|
||||
"form.username_change_not_local_user": "Niet-lokale gebruikers mogen hun gebruikersnaam niet wijzigen.",
|
||||
"form.repo_name_been_taken": "De repository-naam wordt al gebruikt.",
|
||||
"form.repository_force_private": "Forceer privé is ingeschakeld: privé repositories kunnen niet openbaar worden gemaakt.",
|
||||
"form.repository_force_private": "Forceer priv\u00c3\u00a9 is ingeschakeld: priv\u00c3\u00a9 repositories kunnen niet openbaar worden gemaakt.",
|
||||
"form.repository_files_already_exist": "Er bestaan al bestanden voor deze repository. Neem contact op met de systeembeheerder.",
|
||||
"form.repository_files_already_exist.delete": "Er bestaan al bestanden voor deze repository. U moet deze verwijderen.",
|
||||
"form.repository_files_already_exist.adopt_or_delete": "Er bestaan al bestanden voor deze repository. Adopteer of verwijder deze.",
|
||||
"form.visit_rate_limit": "Bezoeklimiet op afstand gerichter.",
|
||||
"form.org_name_been_taken": "Naam van de organisatie wordt al gebruikt.",
|
||||
"form.team_name_been_taken": "De teamnaam is al in gebruik.",
|
||||
"form.team_no_units_error": "Toegang verlenen tot ten minste één repository sectie.",
|
||||
"form.team_no_units_error": "Toegang verlenen tot ten minste \u00c3\u00a9\u00c3\u00a9n repository sectie.",
|
||||
"form.email_been_used": "Het emailadres is al in gebruik.",
|
||||
"form.email_invalid": "Het e-mailadres is ongeldig.",
|
||||
"form.username_password_incorrect": "Gebruikersnaam of wachtwoord is onjuist.",
|
||||
"form.password_complexity": "Wachtwoord voldoet niet aan complexiteit eisen:",
|
||||
"form.password_lowercase_one": "Minstens één kleine letter",
|
||||
"form.password_uppercase_one": "Minstens één hoofdletter",
|
||||
"form.password_digit_one": "Minstens één cijfer",
|
||||
"form.password_special_one": "Minstens één speciaal teken (interpunctie, haakjes, aanhalingstekens, etc.)",
|
||||
"form.password_lowercase_one": "Minstens \u00c3\u00a9\u00c3\u00a9n kleine letter",
|
||||
"form.password_uppercase_one": "Minstens \u00c3\u00a9\u00c3\u00a9n hoofdletter",
|
||||
"form.password_digit_one": "Minstens \u00c3\u00a9\u00c3\u00a9n cijfer",
|
||||
"form.password_special_one": "Minstens \u00c3\u00a9\u00c3\u00a9n speciaal teken (interpunctie, haakjes, aanhalingstekens, etc.)",
|
||||
"form.enterred_invalid_repo_name": "De repository-naam die u hebt ingevoerd is niet correct.",
|
||||
"form.enterred_invalid_org_name": "De organizatienaam die u hebt ingevoerd is niet correct.",
|
||||
"form.enterred_invalid_owner_name": "De nieuwe eigenaarnaam is niet geldig.",
|
||||
"form.enterred_invalid_password": "Het ingevoerde wachtwoord is onjuist.",
|
||||
"form.user_not_exist": "De gebruiker bestaat niet.",
|
||||
"form.team_not_exist": "Dit team bestaat niet.",
|
||||
"form.last_org_owner": "Je kunt de laatste eigenaar van een organisatie niet verwijderen. Er moet er minimaal één eigenaar in een organisatie zitten.",
|
||||
"form.last_org_owner": "Je kunt de laatste eigenaar van een organisatie niet verwijderen. Er moet er minimaal \u00c3\u00a9\u00c3\u00a9n eigenaar in een organisatie zitten.",
|
||||
"form.cannot_add_org_to_team": "Een organisatie kan niet worden toegevoegd als een teamlid.",
|
||||
"form.invalid_ssh_key": "Kan de SSH-sleutel niet verifiëren: %s",
|
||||
"form.invalid_gpg_key": "Kan de GPG-sleutel niet verifiëren: %s",
|
||||
"form.invalid_ssh_key": "Kan de SSH-sleutel niet verifi\u00c3\u00abren: %s",
|
||||
"form.invalid_gpg_key": "Kan de GPG-sleutel niet verifi\u00c3\u00abren: %s",
|
||||
"form.invalid_ssh_principal": "Ongeldige verantwoordelijke: %s",
|
||||
"form.auth_failed": "Verificatie mislukt: %v",
|
||||
"form.target_branch_not_exist": "Doel branch bestaat niet",
|
||||
"user.change_avatar": "Wijzig je profielfoto…",
|
||||
"user.change_avatar": "Wijzig je profielfoto\u00e2\u20ac\u00a6",
|
||||
"user.repositories": "Repository's",
|
||||
"user.activity": "Activiteit",
|
||||
"user.followers": "Volgers",
|
||||
@@ -430,7 +430,7 @@
|
||||
"settings.enable_custom_avatar": "Aangepaste avatar inschakelen",
|
||||
"settings.choose_new_avatar": "Kies een nieuwe avatar",
|
||||
"settings.delete_current_avatar": "Verwijder huidige avatar",
|
||||
"settings.uploaded_avatar_not_a_image": "Het geüploade bestand is geen afbeelding.",
|
||||
"settings.uploaded_avatar_not_a_image": "Het ge\u00c3\u00bcploade bestand is geen afbeelding.",
|
||||
"settings.update_avatar_success": "Je avatar is bijgewerkt.",
|
||||
"settings.update_user_avatar_success": "De avatar van de gebruiker is bijgewerkt.",
|
||||
"settings.change_password": "Wachtwoord bijwerken",
|
||||
@@ -473,7 +473,7 @@
|
||||
"settings.add_key": "Sleutel toevoegen",
|
||||
"settings.ssh_desc": "Deze publieke SSH sleutels worden geassocieerd met uw account. De bijbehorende private sleutels geven volledige toegang toe tot je repositories.",
|
||||
"settings.principal_desc": "Deze SSH-certificaatverantwoordelijken zijn gekoppeld aan uw account en geven volledige toegang tot uw repositories.",
|
||||
"settings.gpg_desc": "Deze publieke GPG-sleutels zijn verbonden met je account. Houd je privé-sleutels veilig, omdat hiermee commits kunnen worden ondertekend.",
|
||||
"settings.gpg_desc": "Deze publieke GPG-sleutels zijn verbonden met je account. Houd je priv\u00c3\u00a9-sleutels veilig, omdat hiermee commits kunnen worden ondertekend.",
|
||||
"settings.ssh_helper": "<strong>Weet u niet hoe?</strong> Lees dan onze handleiding voor het <a href=\"%s\"> genereren van SSH sleutels</a> of voor <a href=\"%s\"> algemene SSH</a> problemen.",
|
||||
"settings.gpg_helper": "<strong>Hulp nodig?</strong> Neem een kijkje op de GitHub handleiding <a href=\"%s\">over GPG</a>.",
|
||||
"settings.add_new_key": "SSH sleutel toevoegen",
|
||||
@@ -489,15 +489,15 @@
|
||||
"settings.gpg_key_matched_identities": "Overeenkomende identiteiten:",
|
||||
"settings.gpg_key_matched_identities_long": "De ingesloten identiteiten in deze sleutel komen overeen met de geactiveerde e-mailadressen voor deze gebruiker. Commits die overeenkomen met deze e-mailadressen kunnen worden geverifieerd met deze sleutel.",
|
||||
"settings.gpg_key_verified": "Geverifieerde sleutel",
|
||||
"settings.gpg_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifiëren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker naast de bijbehorende identiteiten voor deze sleutel.",
|
||||
"settings.gpg_key_verify": "Verifiëren",
|
||||
"settings.gpg_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifi\u00c3\u00abren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker naast de bijbehorende identiteiten voor deze sleutel.",
|
||||
"settings.gpg_key_verify": "Verifi\u00c3\u00abren",
|
||||
"settings.gpg_token_required": "U moet een handtekening opgeven voor de onderstaande token",
|
||||
"settings.gpg_token_help": "U kunt een handtekening genereren met:",
|
||||
"settings.gpg_token_signature": "Gepantserde GPG-handtekening",
|
||||
"settings.key_signature_gpg_placeholder": "Begint met '-----BEGIN PGP SIGNATURE-----'",
|
||||
"settings.ssh_key_verified": "Geverifieerde sleutel",
|
||||
"settings.ssh_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifiëren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker.",
|
||||
"settings.ssh_key_verify": "Verifiëren",
|
||||
"settings.ssh_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifi\u00c3\u00abren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker.",
|
||||
"settings.ssh_key_verify": "Verifi\u00c3\u00abren",
|
||||
"settings.ssh_token_required": "U moet een handtekening opgeven voor het onderstaande token",
|
||||
"settings.ssh_token_help": "U kunt een handtekening genereren door het volgende:",
|
||||
"settings.ssh_token_signature": "Gepantserde SSH handtekening",
|
||||
@@ -597,7 +597,7 @@
|
||||
"settings.visibility": "Gebruiker zichtbaarheid",
|
||||
"settings.visibility.public": "Openbaar",
|
||||
"settings.visibility.limited": "Beperkt",
|
||||
"settings.visibility.private": "Privé",
|
||||
"settings.visibility.private": "Priv\u00c3\u00a9",
|
||||
"repo.owner": "Eigenaar",
|
||||
"repo.owner_helper": "Sommige organisaties kunnen niet worden weergegeven in de dropdown vanwege een limiet op het maximale aantal repositories.",
|
||||
"repo.repo_name": "Naam van repository",
|
||||
@@ -608,7 +608,7 @@
|
||||
"repo.template_description": "Sjabloon repositories laten gebruikers nieuwe repositories genereren met dezelfde directory structuur, bestanden en optionele instellingen.",
|
||||
"repo.visibility": "Zichtbaarheid",
|
||||
"repo.visibility_description": "Alleen de eigenaar of de organisatielid kan het zien als ze rechten hebben.",
|
||||
"repo.visibility_helper_forced": "De sitebeheerder verplicht alle repositories om privé te zijn.",
|
||||
"repo.visibility_helper_forced": "De sitebeheerder verplicht alle repositories om priv\u00c3\u00a9 te zijn.",
|
||||
"repo.visibility_fork_helper": "(Verandering van deze waarde zal van invloed zijn op alle forks)",
|
||||
"repo.clone_helper": "Heb je hulp nodig om te clonen? Bekijk dan de <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">handleiding</a>.",
|
||||
"repo.fork_repo": "Repository forken",
|
||||
@@ -671,18 +671,18 @@
|
||||
"repo.blame_prior": "Bekijk de schuld voorafgaand aan deze verandering",
|
||||
"repo.transfer.accept": "Accepteer overdracht",
|
||||
"repo.transfer.reject": "Overdracht afwijzen",
|
||||
"repo.desc.private": "Privé",
|
||||
"repo.desc.private": "Priv\u00c3\u00a9",
|
||||
"repo.desc.public": "Openbaar",
|
||||
"repo.desc.template": "Sjabloon",
|
||||
"repo.desc.internal": "Interne",
|
||||
"repo.desc.archived": "Gearchiveerd",
|
||||
"repo.template.items": "Sjabloon items",
|
||||
"repo.template.git_content": "Git inhoud (standaard Branch)",
|
||||
"repo.template.git_hooks_tooltip": "Je bent momenteel niet in staat om Git Hooks één keer te wijzigen of te verwijderen. Selecteer deze optie alleen als je de sjabloonrepository vertrouwt.",
|
||||
"repo.template.git_hooks_tooltip": "Je bent momenteel niet in staat om Git Hooks \u00c3\u00a9\u00c3\u00a9n keer te wijzigen of te verwijderen. Selecteer deze optie alleen als je de sjabloonrepository vertrouwt.",
|
||||
"repo.template.topics": "Onderwerpen",
|
||||
"repo.template.avatar": "Profielfoto",
|
||||
"repo.template.issue_labels": "Issuelabels",
|
||||
"repo.template.one_item": "Moet ten minste één sjabloon selecteren",
|
||||
"repo.template.one_item": "Moet ten minste \u00c3\u00a9\u00c3\u00a9n sjabloon selecteren",
|
||||
"repo.template.invalid": "Moet een sjabloon repository selecteren",
|
||||
"repo.archive.issue.nocomment": "Deze repo is gearchiveerd. U kunt niet reageren op problemen.",
|
||||
"repo.archive.pull.nocomment": "Deze repo is gearchiveerd. U kunt niet reageren op pull requests.",
|
||||
@@ -773,7 +773,7 @@
|
||||
"repo.invisible_runes_line": "Deze lijn heeft onzichtbare unicode karakters",
|
||||
"repo.ambiguous_runes_line": "Deze lijn heeft dubbelzinnige unicode karakters",
|
||||
"repo.unescape_control_characters": "Onescape",
|
||||
"repo.file_copy_permalink": "Permalink kopiëren",
|
||||
"repo.file_copy_permalink": "Permalink kopi\u00c3\u00abren",
|
||||
"repo.view_git_blame": "Bekijk Git Blame",
|
||||
"repo.video_not_supported_in_browser": "Je browser ondersteunt de HTML5 'video'-tag niet.",
|
||||
"repo.audio_not_supported_in_browser": "Je browser ondersteunt de HTML5 'audio'-tag niet.",
|
||||
@@ -799,7 +799,7 @@
|
||||
"repo.editor.fork_before_edit": "Je moet deze repository forken om veranderingen te maken of voor te stellen.",
|
||||
"repo.editor.delete_this_file": "Verwijder bestand",
|
||||
"repo.editor.must_have_write_access": "U moet schrijftoegang hebben om aanpassingen te maken of voor te stellen in dit bestand.",
|
||||
"repo.editor.name_your_file": "Bestandsnaam…",
|
||||
"repo.editor.name_your_file": "Bestandsnaam\u00e2\u20ac\u00a6",
|
||||
"repo.editor.filename_help": "Voeg een map toe door zijn naam te typen, gevolgd door een slash ('/'). Verwijder een map door op backspace te drukken aan het begin van het tekstveld.",
|
||||
"repo.editor.or": "of",
|
||||
"repo.editor.cancel_lower": "Annuleer",
|
||||
@@ -809,13 +809,13 @@
|
||||
"repo.editor.patching": "Patchen:",
|
||||
"repo.editor.new_patch": "Nieuwe Patch",
|
||||
"repo.editor.commit_message": "Commit-bericht",
|
||||
"repo.editor.commit_message_desc": "Voeg een optionele uitgebreide omschrijving toe…",
|
||||
"repo.editor.commit_message_desc": "Voeg een optionele uitgebreide omschrijving toe\u00e2\u20ac\u00a6",
|
||||
"repo.editor.signoff_desc": "Voeg een Signed-off-by toe aan het einde van het commit logbericht.",
|
||||
"repo.editor.commit_directly_to_this_branch": "Commit direct naar de branch '<strong class=\"branch-name\">%s</strong>'.",
|
||||
"repo.editor.create_new_branch": "Maak een <strong>nieuwe branch</strong> voor deze commit en start van een pull-aanvraag.",
|
||||
"repo.editor.create_new_branch_np": "Maak een <strong>nieuwe branch</strong> voor deze commit.",
|
||||
"repo.editor.propose_file_change": "Stel bestandswijziging voor",
|
||||
"repo.editor.new_branch_name_desc": "Nieuwe branch naam…",
|
||||
"repo.editor.new_branch_name_desc": "Nieuwe branch naam\u00e2\u20ac\u00a6",
|
||||
"repo.editor.cancel": "Annuleer",
|
||||
"repo.editor.filename_cannot_be_empty": "Bestandsnaam mag niet leeg zijn.",
|
||||
"repo.editor.file_changed_while_editing": "De bestandsinhoud is veranderd sinds je bent begonnen met bewerken. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Klik hier</a> om ze te zien, of <strong>commit de veranderingen opnieuw</strong> om ze te overschrijven.",
|
||||
@@ -825,7 +825,7 @@
|
||||
"repo.editor.push_rejected_no_message": "De wijziging is afgewezen door de server zonder bericht. Controleer de Git Hooks alsjeblieft.",
|
||||
"repo.editor.push_rejected": "De wijziging is afgewezen door de server. Controleer Controleer de Git Hooks alsjeblieft.",
|
||||
"repo.editor.push_rejected_summary": "Volledig afwijzingsbericht:",
|
||||
"repo.editor.add_subdir": "Een map toevoegen…",
|
||||
"repo.editor.add_subdir": "Een map toevoegen\u00e2\u20ac\u00a6",
|
||||
"repo.editor.no_commit_to_branch": "Kan niet rechtstreeks naar branch committen omdat:",
|
||||
"repo.editor.user_no_push_to_branch": "Gebruiker kan niet pushen naar branch",
|
||||
"repo.editor.require_signed_commit": "Branch vereist een ondertekende commit",
|
||||
@@ -857,7 +857,7 @@
|
||||
"repo.projects.create": "Project aanmaken",
|
||||
"repo.projects.title": "Titel",
|
||||
"repo.projects.new": "Nieuw project",
|
||||
"repo.projects.new_subheader": "Coördineer, track en update uw werk op één plek, dus projecten blijven transparant en op schema.",
|
||||
"repo.projects.new_subheader": "Co\u00c3\u00b6rdineer, track en update uw werk op \u00c3\u00a9\u00c3\u00a9n plek, dus projecten blijven transparant en op schema.",
|
||||
"repo.projects.deletion": "Project verwijderen",
|
||||
"repo.projects.deletion_desc": "Als een project wordt verwijdert, wordt deze van alle gerelateerde kwesties verwijderd. Doorgaan?",
|
||||
"repo.projects.deletion_success": "Het project is verwijderd.",
|
||||
@@ -971,7 +971,7 @@
|
||||
"repo.issues.num_comments": "%d opmerkingen",
|
||||
"repo.issues.commented_at": "reageerde <a href=\"#%s\">%s</a>",
|
||||
"repo.issues.delete_comment_confirm": "Weet u zeker dat u deze reactie wilt verwijderen?",
|
||||
"repo.issues.context.copy_link": "Link kopiëren",
|
||||
"repo.issues.context.copy_link": "Link kopi\u00c3\u00abren",
|
||||
"repo.issues.context.quote_reply": "Citeer antwoord",
|
||||
"repo.issues.context.reference_issue": "Verwijs in nieuw issue",
|
||||
"repo.issues.context.edit": "Bewerken",
|
||||
@@ -1071,7 +1071,7 @@
|
||||
"repo.issues.dependency.title": "Afhankelijkheden",
|
||||
"repo.issues.dependency.issue_no_dependencies": "Geen afhankelijkheden ingesteld.",
|
||||
"repo.issues.dependency.pr_no_dependencies": "Geen afhankelijkheden ingesteld.",
|
||||
"repo.issues.dependency.add": "Voeg afhankelijkheid toe…",
|
||||
"repo.issues.dependency.add": "Voeg afhankelijkheid toe\u00e2\u20ac\u00a6",
|
||||
"repo.issues.dependency.cancel": "Annuleer",
|
||||
"repo.issues.dependency.remove": "Verwijder",
|
||||
"repo.issues.dependency.remove_info": "Verwijder afhankelijkheid",
|
||||
@@ -1115,7 +1115,7 @@
|
||||
"repo.issues.reference_issue.body": "Inhoud",
|
||||
"repo.issues.content_history.deleted": "verwijderd",
|
||||
"repo.issues.content_history.edited": "bewerkt",
|
||||
"repo.issues.content_history.created": "gecreëerd",
|
||||
"repo.issues.content_history.created": "gecre\u00c3\u00aberd",
|
||||
"repo.issues.content_history.delete_from_history": "Uit geschiedenis verwijderen",
|
||||
"repo.issues.content_history.delete_from_history_confirm": "Uit geschiedenis verwijderen?",
|
||||
"repo.issues.content_history.options": "Opties",
|
||||
@@ -1251,7 +1251,7 @@
|
||||
"repo.wiki.back_to_wiki": "Terug naar wiki-pagina",
|
||||
"repo.wiki.delete_page_button": "Verwijder pagina",
|
||||
"repo.wiki.page_already_exists": "Er bestaat al een wiki-pagina met deze naam.",
|
||||
"repo.wiki.pages": "Pagina’s",
|
||||
"repo.wiki.pages": "Pagina\u00e2\u20ac\u2122s",
|
||||
"repo.wiki.last_updated": "Laatst bijgewerkt: %s",
|
||||
"repo.wiki.page_name_desc": "Voer een naam in voor deze Wiki pagina. Sommige speciale namen zijn: 'Home', '_Sidebar' en '_Footer'.",
|
||||
"repo.activity": "Activiteit",
|
||||
@@ -1411,9 +1411,9 @@
|
||||
"repo.settings.discord_icon_url": "Icoon URL",
|
||||
"repo.settings.event_desc": "Trigger op:",
|
||||
"repo.settings.event_send_everything": "Alle gebeurtenissen",
|
||||
"repo.settings.event_choose": "Aangepaste gebeurtenissen…",
|
||||
"repo.settings.event_choose": "Aangepaste gebeurtenissen\u00e2\u20ac\u00a6",
|
||||
"repo.settings.event_header_repository": "Repository gebeurtenissen",
|
||||
"repo.settings.event_create": "Creëer",
|
||||
"repo.settings.event_create": "Cre\u00c3\u00aber",
|
||||
"repo.settings.event_create_desc": "Branch, of tag aangemaakt.",
|
||||
"repo.settings.event_delete": "Verwijder",
|
||||
"repo.settings.event_delete_desc": "Branch of tag verwijderd.",
|
||||
@@ -1487,13 +1487,13 @@
|
||||
"repo.settings.require_signed_commits_desc": "Weiger pushes naar deze branch als deze niet ondertekend of niet verifieerbaar is.",
|
||||
"repo.settings.protected_branch_deletion_desc": "Branch bescherming uitschakelen zorgt ervoor dat gebruikers met schrijfrechten naar de branch kunnen pushen. Doorgaan?",
|
||||
"repo.settings.block_rejected_reviews": "Samenvoegen van afgewezen beoordelingen blokkeren",
|
||||
"repo.settings.block_rejected_reviews_desc": "Samenvoegen zal niet mogelijk zijn wanneer er wijzigingen worden aangevraagd door officiële beoordelaars, zelfs niet als er genoeg goedkeuringen zijn.",
|
||||
"repo.settings.block_on_official_review_requests": "Blokkeer de samenvoeging van officiële beoordelingsverzoeken",
|
||||
"repo.settings.block_on_official_review_requests_desc": "Samenvoegen is niet mogelijk wanneer het officiële herzieningsverzoeken heeft, ook al zijn er genoeg goedkeuringen.",
|
||||
"repo.settings.block_rejected_reviews_desc": "Samenvoegen zal niet mogelijk zijn wanneer er wijzigingen worden aangevraagd door offici\u00c3\u00able beoordelaars, zelfs niet als er genoeg goedkeuringen zijn.",
|
||||
"repo.settings.block_on_official_review_requests": "Blokkeer de samenvoeging van offici\u00c3\u00able beoordelingsverzoeken",
|
||||
"repo.settings.block_on_official_review_requests_desc": "Samenvoegen is niet mogelijk wanneer het offici\u00c3\u00able herzieningsverzoeken heeft, ook al zijn er genoeg goedkeuringen.",
|
||||
"repo.settings.block_outdated_branch": "Samenvoegen blokkeren als pull request verouderd is",
|
||||
"repo.settings.block_outdated_branch_desc": "Samenvoegen is niet mogelijk als de hoofd branch achter loop op de basis branch.",
|
||||
"repo.settings.default_branch_desc": "Selecteer een standaard repository branch voor pull requests en code commits:",
|
||||
"repo.settings.choose_branch": "Kies een branch…",
|
||||
"repo.settings.choose_branch": "Kies een branch\u00e2\u20ac\u00a6",
|
||||
"repo.settings.no_protected_branch": "Er zijn geen beschermde branches.",
|
||||
"repo.settings.edit_protected_branch": "Bewerken",
|
||||
"repo.settings.protected_branch_required_approvals_min": "Vereiste goedkeuringen kunnen niet negatief zijn.",
|
||||
@@ -1572,7 +1572,7 @@
|
||||
"repo.diff.load": "Laad Diff",
|
||||
"repo.diff.generated": "gegenereerd",
|
||||
"repo.diff.comment.placeholder": "Opmerking toevoegen",
|
||||
"repo.diff.comment.add_single_comment": "Één reactie toevoegen",
|
||||
"repo.diff.comment.add_single_comment": "\u00c3\u2030\u00c3\u00a9n reactie toevoegen",
|
||||
"repo.diff.comment.add_review_comment": "Voeg commentaar toe",
|
||||
"repo.diff.comment.start_review": "Review starten",
|
||||
"repo.diff.comment.reply": "Reageer",
|
||||
@@ -1600,7 +1600,7 @@
|
||||
"repo.release.source_code": "Broncode",
|
||||
"repo.release.tag_name": "Tagnaam",
|
||||
"repo.release.target": "Doel",
|
||||
"repo.release.tag_helper": "Kies een bestaande tag, of creëer een nieuwe tag bij publiceren.",
|
||||
"repo.release.tag_helper": "Kies een bestaande tag, of cre\u00c3\u00aber een nieuwe tag bij publiceren.",
|
||||
"repo.release.prerelease_desc": "Markeren als voorlopige versie",
|
||||
"repo.release.prerelease_helper": "Markeer deze release als ongeschikt voor productiedoeleinden.",
|
||||
"repo.release.cancel": "Annuleren",
|
||||
@@ -1652,8 +1652,8 @@
|
||||
"org.settings.visibility": "Zichtbaarheid",
|
||||
"org.settings.visibility.public": "Publiek",
|
||||
"org.settings.visibility.limited_shortname": "Beperkt",
|
||||
"org.settings.visibility.private": "Privé (alleen zichtbaar voor organisatieleden)",
|
||||
"org.settings.visibility.private_shortname": "Privé",
|
||||
"org.settings.visibility.private": "Priv\u00c3\u00a9 (alleen zichtbaar voor organisatieleden)",
|
||||
"org.settings.visibility.private_shortname": "Priv\u00c3\u00a9",
|
||||
"org.settings.update_settings": "Instellingen bijwerken",
|
||||
"org.settings.update_avatar_success": "De avatar van de organisatie is aangepast.",
|
||||
"org.settings.delete": "Verwijder organisatie",
|
||||
@@ -1807,7 +1807,7 @@
|
||||
"admin.users.allow_create_organization": "Mag organisaties aanmaken",
|
||||
"admin.users.update_profile": "Update gebruikers account",
|
||||
"admin.users.delete_account": "Verwijder gebruikers account",
|
||||
"admin.users.still_own_repo": "Deze gebruiker is nog steeds eigenaar van één of meerdere repositories. Verwijder of draag eerst deze repositories over.",
|
||||
"admin.users.still_own_repo": "Deze gebruiker is nog steeds eigenaar van \u00c3\u00a9\u00c3\u00a9n of meerdere repositories. Verwijder of draag eerst deze repositories over.",
|
||||
"admin.users.still_has_org": "Deze gebruiker is lid van een organisatie. Verwijder de gebruiker eerst uit alle organisaties.",
|
||||
"admin.users.deletion_success": "De gebruiker is verwijderd.",
|
||||
"admin.users.list_status_filter.is_active": "Actief",
|
||||
@@ -2224,24 +2224,24 @@
|
||||
"search.search": "Zoeken",
|
||||
"search.fuzzy_tooltip": "Include results that closely match the search term",
|
||||
"search.regexp": "Regexp",
|
||||
"search.repo_kind": "Search repos…",
|
||||
"search.user_kind": "Search users…",
|
||||
"search.org_kind": "Search orgs…",
|
||||
"search.team_kind": "Search teams…",
|
||||
"search.code_kind": "Search code…",
|
||||
"search.package_kind": "Search packages…",
|
||||
"search.project_kind": "Search projects…",
|
||||
"search.branch_kind": "Search branches…",
|
||||
"search.tag_kind": "Search tags…",
|
||||
"search.commit_kind": "Search commits…",
|
||||
"search.runner_kind": "Search runners…",
|
||||
"search.issue_kind": "Search issues…",
|
||||
"search.pull_kind": "Search pull requests…",
|
||||
"search.repo_kind": "Search repos\u2026",
|
||||
"search.user_kind": "Search users\u2026",
|
||||
"search.org_kind": "Search orgs\u2026",
|
||||
"search.team_kind": "Search teams\u2026",
|
||||
"search.code_kind": "Search code\u2026",
|
||||
"search.package_kind": "Search packages\u2026",
|
||||
"search.project_kind": "Search projects\u2026",
|
||||
"search.branch_kind": "Search branches\u2026",
|
||||
"search.tag_kind": "Search tags\u2026",
|
||||
"search.commit_kind": "Search commits\u2026",
|
||||
"search.runner_kind": "Search runners\u2026",
|
||||
"search.issue_kind": "Search issues\u2026",
|
||||
"search.pull_kind": "Search pull requests\u2026",
|
||||
"aria.footer.links": "Links",
|
||||
"editor.buttons.strikethrough.tooltip": "Add strikethrough text",
|
||||
"filter.string.asc": "A–Z",
|
||||
"filter.string.desc": "Z–A",
|
||||
"install.installing_desc": "Installing now, please wait…",
|
||||
"filter.string.asc": "A\u2013Z",
|
||||
"filter.string.desc": "Z\u2013A",
|
||||
"install.installing_desc": "Installing now, please wait\u2026",
|
||||
"install.db_schema": "Schema",
|
||||
"install.ssl_mode": "SSL",
|
||||
"install.reinstall_confirm_check_1": "The data encrypted by the SECRET_KEY in app.ini may be lost: users may not be able to log in with 2FA/OTP and mirrors may not function correctly. By checking this box, you confirm that the current app.ini file contains the correct SECRET_KEY.",
|
||||
@@ -2322,7 +2322,7 @@
|
||||
"repo.mirror_address_url_invalid": "The provided URL is invalid. Make sure all components of the URL are escaped correctly.",
|
||||
"repo.mirror_lfs_endpoint_desc": "Sync will attempt to use the clone URL to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">determine the LFS server</a>. You can also specify a custom endpoint if the repository LFS data is stored somewhere else.",
|
||||
"repo.forks": "Forks",
|
||||
"repo.adopt_search": "Enter username to search for unadopted repositories… (leave blank to find all)",
|
||||
"repo.adopt_search": "Enter username to search for unadopted repositories\u2026 (leave blank to find all)",
|
||||
"repo.desc.sha256": "SHA256",
|
||||
"repo.template.webhooks": "Webhooks",
|
||||
"repo.archive.title": "This repo is archived. You can view files and clone it. You cannot open issues or pull requests or push a commit.",
|
||||
@@ -2333,7 +2333,7 @@
|
||||
"repo.migrate_items_releases": "Releases",
|
||||
"repo.migrate.github_token_desc": "You can put one or more tokens here, separated by commas, to make migrating faster by circumventing the GitHub API rate limit. WARNING: Abusing this feature may violate the service provider's policy and may lead to getting your account(s) blocked.",
|
||||
"repo.migrate.permission_denied_blocked": "You cannot import from disallowed hosts. Please ask the admin to check ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS settings.",
|
||||
"repo.migrate.migrating": "Migrating from <b>%s</b>…",
|
||||
"repo.migrate.migrating": "Migrating from <b>%s</b>\u2026",
|
||||
"repo.migrate.codecommit.aws_access_key_id": "AWS Access Key ID",
|
||||
"repo.migrate.codecommit.aws_secret_access_key": "AWS Secret Access Key",
|
||||
"repo.migration_status": "Migration status",
|
||||
@@ -2388,14 +2388,14 @@
|
||||
"repo.issues.review.review": "Review",
|
||||
"repo.issues.assignee.error": "Not all assignees were added, due to an unexpected error.",
|
||||
"repo.compare.title": "Comparing changes",
|
||||
"repo.compare.description": "Choose two branches or tags to see what’s changed or to start a new pull request.",
|
||||
"repo.compare.description": "Choose two branches or tags to see what\u2019s changed or to start a new pull request.",
|
||||
"repo.pulls.new.description": "Discuss and review the changes in this comparison with others.",
|
||||
"repo.pulls.new.already_existed": "A pull request between these branches already exists",
|
||||
"repo.pulls.edit.already_changed": "Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.",
|
||||
"repo.pulls.select_commit_hold_shift_for_range": "Select commit. Hold Shift and click to select a range.",
|
||||
"repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.",
|
||||
"repo.pulls.tab_commits": "Commits",
|
||||
"repo.pulls.is_checking": "Checking for merge conflicts…",
|
||||
"repo.pulls.is_checking": "Checking for merge conflicts\u2026",
|
||||
"repo.pulls.wrong_commit_id": "commit ID must be a commit ID on the target branch",
|
||||
"repo.pulls.no_merge_not_ready": "This pull request is not ready to be merged. Check review status and status checks.",
|
||||
"repo.pulls.rebase_merge_pull_request": "Rebase, then fast-forward",
|
||||
@@ -2413,7 +2413,7 @@
|
||||
"repo.pulls.status_checks_need_approvals_helper": "The workflow will only run after approval from the repository maintainer.",
|
||||
"repo.pulls.cmd_instruction_checkout_title": "Checkout",
|
||||
"repo.pulls.cmd_instruction_merge_warning": "Warning: This operation cannot merge pull request because \"autodetect manual merge\" is not enabled.",
|
||||
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By…\".",
|
||||
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By\u2026\".",
|
||||
"repo.signing.wont_sign.error": "There was an error while checking if the commit could be signed.",
|
||||
"repo.signing.wont_sign.twofa": "You must have two-factor authentication enabled to have commits signed.",
|
||||
"repo.wiki": "Wiki",
|
||||
@@ -2474,8 +2474,8 @@
|
||||
"repo.settings.visibility.private.bullet_two": "May remove the relationship between it and <strong>forks</strong>, <strong>watchers</strong>, and <strong>stars</strong>.",
|
||||
"repo.settings.unarchive.text": "Unarchiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull requests.",
|
||||
"repo.settings.lfs": "LFS",
|
||||
"repo.settings.lfs_lock_path": "Filepath to lock…",
|
||||
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) — %d associated, %d unassociated (%d missing from store)",
|
||||
"repo.settings.lfs_lock_path": "Filepath to lock\u2026",
|
||||
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) \u2014 %d associated, %d unassociated (%d missing from store)",
|
||||
"repo.settings.lfs_pointers.sha": "Blob SHA",
|
||||
"repo.settings.lfs_pointers.oid": "OID",
|
||||
"repo.settings.pages": "Pagina's",
|
||||
@@ -2535,7 +2535,7 @@
|
||||
"repo.branch.commits_divergence_from": "Commit divergence: %[1]d behind and %[2]d ahead of %[3]s",
|
||||
"repo.branch.commits_no_divergence": "The same as branch %[1]s",
|
||||
"repo.find_file.follow_symlink": "Follow this symlink to where it is pointing at",
|
||||
"graphs.component_loading": "Loading %s…",
|
||||
"graphs.component_loading": "Loading %s\u2026",
|
||||
"org.teams": "Teams",
|
||||
"org.pinned_repos": "Featured Projects",
|
||||
"org.public_members": "Public Members",
|
||||
@@ -2544,7 +2544,7 @@
|
||||
"org.settings.email": "Contact Email Address",
|
||||
"org.settings.change_visibility": "Change Visibility",
|
||||
"org.settings.change_visibility_notices_1": "If the organization is converted to private, the repository stars will be removed and cannot be restored.",
|
||||
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organization’s repositories if visibility is changed to private.",
|
||||
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organization\u2019s repositories if visibility is changed to private.",
|
||||
"org.settings.change_visibility_success": "The visibility of organization %s has been successfully changed.",
|
||||
"org.settings.visibility_desc": "Change who can view the organization and its repositories.",
|
||||
"org.settings.rename": "Rename Organization",
|
||||
@@ -2660,12 +2660,12 @@
|
||||
"packages.settings.global_access.disabled": "Globale toegang tot pakket is uitgeschakeld.",
|
||||
"packages.settings.global_access.error": "Kon globale toegangsinstelling niet bijwerken.",
|
||||
"packages.visibility": "Zichtbaarheid",
|
||||
"packages.settings.visibility.private.text": "Dit pakket is momenteel privé. Maak het openbaar om iedereen toegang te geven.",
|
||||
"packages.settings.visibility.private.button": "Privé maken",
|
||||
"packages.settings.visibility.private.bullet_title": "U staat op het punt dit pakket privé te maken.",
|
||||
"packages.settings.visibility.private.text": "Dit pakket is momenteel priv\u00e9. Maak het openbaar om iedereen toegang te geven.",
|
||||
"packages.settings.visibility.private.button": "Priv\u00e9 maken",
|
||||
"packages.settings.visibility.private.bullet_title": "U staat op het punt dit pakket priv\u00e9 te maken.",
|
||||
"packages.settings.visibility.private.bullet_one": "Alleen gebruikers met de juiste rechten kunnen dit pakket openen.",
|
||||
"packages.settings.visibility.private.success": "Pakket is nu privé.",
|
||||
"packages.settings.visibility.public.text": "Dit pakket is momenteel openbaar. Maak het privé om de toegang te beperken.",
|
||||
"packages.settings.visibility.private.success": "Pakket is nu priv\u00e9.",
|
||||
"packages.settings.visibility.public.text": "Dit pakket is momenteel openbaar. Maak het priv\u00e9 om de toegang te beperken.",
|
||||
"packages.settings.visibility.public.button": "Openbaar maken",
|
||||
"packages.settings.visibility.public.bullet_title": "U staat op het punt dit pakket openbaar te maken.",
|
||||
"packages.settings.visibility.public.bullet_one": "Iedereen kan dit pakket openen en downloaden.",
|
||||
@@ -2714,7 +2714,7 @@
|
||||
"actions.general.collaborative_owner_not_exist": "Collaboratieve eigenaar bestaat niet",
|
||||
"actions.general.remove_collaborative_owner": "Collaboratieve eigenaar verwijderen",
|
||||
"actions.general.remove_collaborative_owner_desc": "De eigenaar verliest toegang tot runnerbeheer",
|
||||
"git.filemode.changed_filemode": "%[1]s → %[2]s",
|
||||
"git.filemode.changed_filemode": "%[1]s \u2192 %[2]s",
|
||||
"org.pinned_repos_empty_title": "Showcase your best work",
|
||||
"org.pinned_repos_empty_desc": "Pin up to 6 repositories to highlight your organization's most important projects.",
|
||||
"org.settings.pinned.manage": "Manage Pins",
|
||||
@@ -2781,7 +2781,7 @@
|
||||
"admin.config.pinned_org_format_condensed": "Compact",
|
||||
"admin.config.pinned_org_format_regular": "Normaal",
|
||||
"admin.config.pinned_org_display_format_help": "Weergaveformaat voor vastgepinde organisaties",
|
||||
"admin.config.logo_upload_success": "Logo geüpload",
|
||||
"admin.config.logo_upload_success": "Logo ge\u00fcpload",
|
||||
"admin.config.logo_url_success": "Logo-URL ingesteld",
|
||||
"admin.config.logo_reset_success": "Logo gereset",
|
||||
"admin.config.logo_invalid_type": "Ongeldig logo-bestandstype",
|
||||
@@ -2792,7 +2792,7 @@
|
||||
"admin.config.reset_icon": "Pictogram resetten",
|
||||
"admin.config.current_icon": "Huidig pictogram",
|
||||
"admin.config.icon_url": "Pictogram-URL",
|
||||
"admin.config.icon_upload_success": "Pictogram geüpload",
|
||||
"admin.config.icon_upload_success": "Pictogram ge\u00fcpload",
|
||||
"admin.config.icon_url_success": "Pictogram-URL ingesteld",
|
||||
"admin.config.icon_reset_success": "Pictogram gereset",
|
||||
"admin.config.icon_invalid_type": "Ongeldig bestandstype. Toegestaan: SVG, PNG, ICO",
|
||||
@@ -2884,6 +2884,12 @@
|
||||
"repo.settings.pages.nav_show_repository": "Repository-link tonen (Broncode bekijken-knop)",
|
||||
"repo.settings.pages.nav_show_releases": "Releases-link tonen",
|
||||
"repo.settings.pages.nav_show_issues": "Issues-link tonen",
|
||||
"repo.settings.pages.section_labels": "Sectielabels",
|
||||
"repo.settings.pages.section_labels_desc": "Pas de koppen aan die op uw landingspagina worden weergegeven voor elke sectie",
|
||||
"repo.settings.pages.label_value_props": "Kop waardeproposities",
|
||||
"repo.settings.pages.label_value_props_help": "Kop voor de sectie waardeproposities (bijv., \"Waarom voor ons kiezen\")",
|
||||
"repo.settings.pages.label_features": "Kop functies",
|
||||
"repo.settings.pages.label_features_help": "Kop voor de sectie functies (bijv., \"Mogelijkheden\")",
|
||||
"repo.settings.pages.blog_section": "Blogsectie",
|
||||
"repo.settings.pages.blog_enabled_desc": "Toon recente blogberichten op de landingspagina",
|
||||
"repo.settings.pages.blog_headline": "Blogtitel",
|
||||
@@ -3074,7 +3080,7 @@
|
||||
"repo.settings.gallery_upload": "Afbeeldingen uploaden",
|
||||
"repo.settings.gallery_current": "Huidige afbeeldingen",
|
||||
"repo.settings.gallery_empty": "Nog geen galerij-afbeeldingen. Upload er enkele om uw project te tonen!",
|
||||
"repo.settings.gallery_uploaded": "Afbeelding succesvol geüpload.",
|
||||
"repo.settings.gallery_uploaded": "Afbeelding succesvol ge\u00fcpload.",
|
||||
"repo.settings.gallery_deleted": "Afbeelding succesvol verwijderd.",
|
||||
"repo.settings.gallery_caption_saved": "Bijschrift succesvol opgeslagen.",
|
||||
"repo.settings.gallery_caption_placeholder": "Voeg een bijschrift toe...",
|
||||
@@ -3131,12 +3137,12 @@
|
||||
"vault.back_to_secret": "Terug naar Geheim",
|
||||
"vault.back_to_versions": "Terug naar Versies",
|
||||
"vault.show_value": "Waarde tonen",
|
||||
"vault.copy_value": "Kopiëren naar klembord",
|
||||
"vault.copy_value": "Kopi\u00ebren naar klembord",
|
||||
"vault.view_hidden": "Verborgen bekijken",
|
||||
"vault.view_raw": "Ruwe waarde bekijken",
|
||||
"vault.hidden": "Verborgen",
|
||||
"vault.raw": "Ruw",
|
||||
"vault.copy": "Kopiëren",
|
||||
"vault.copy": "Kopi\u00ebren",
|
||||
"vault.copied": "Gekopieerd!",
|
||||
"vault.multiline_hint": "Ondersteunt meerregelige inhoud (env-bestanden, certificaten, enz.)",
|
||||
"vault.rollback": "Terugdraaien",
|
||||
@@ -3254,5 +3260,18 @@
|
||||
"repo.view_file": "Bestand bekijken",
|
||||
"actions.runners.waiting_jobs": "Wachtende taken",
|
||||
"actions.runners.back_to_runners": "Terug naar runners",
|
||||
"actions.runners.no_waiting_jobs": "Geen taken wachten op dit label"
|
||||
}
|
||||
"actions.runners.no_waiting_jobs": "Geen taken wachten op dit label",
|
||||
"repo.settings.pages.cross_promote_section": "Kruispromotie-sectie",
|
||||
"repo.settings.pages.cross_promote_enabled_desc": "Toon cross-gepromote repositories op de landingspagina",
|
||||
"repo.settings.pages.cross_promote_headline": "Sectiekop",
|
||||
"repo.settings.pages.cross_promote_subheadline": "Sectie-ondertitel",
|
||||
"repo.settings.pages.cross_promote_help": "Configureer de repositories in Instellingen > Kruispromotie.",
|
||||
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files": "Verborgen bestanden en mappen uitsluiten",
|
||||
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc": "Bestanden/mappen die beginnen met \".\" of als verborgen gemarkeerd zijn niet naar de remote mirror pushen",
|
||||
"repo.settings.pages.value_props_headline": "Sectiekop",
|
||||
"repo.settings.pages.value_props_headline_help": "Grote kop voor de sectie waardeproposities. Het kleine label erboven wordt ingesteld in Sectielabels.",
|
||||
"repo.settings.pages.value_props_subheadline": "Sectie-ondertitel",
|
||||
"repo.settings.pages.features_headline": "Sectiekop",
|
||||
"repo.settings.pages.features_headline_help": "Grote kop voor de sectie functies. Het kleine label erboven wordt ingesteld in Sectielabels.",
|
||||
"repo.settings.pages.features_subheadline": "Sectie-ondertitel"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -248,6 +248,26 @@ func Routes() *web.Router {
|
||||
}, reqToken())
|
||||
}, repoAssignment())
|
||||
|
||||
// Organization v2 API - org overview, repos, profile, pinned repos
|
||||
m.Group("/user/orgs", func() {
|
||||
m.Get("", ListUserOrgsV2)
|
||||
}, reqToken())
|
||||
|
||||
m.Group("/orgs/{org}", func() {
|
||||
// Public read endpoints
|
||||
m.Get("/overview", GetOrgOverviewV2)
|
||||
m.Get("/repos", ListOrgReposV2)
|
||||
m.Get("/profile/readme", GetOrgProfileReadmeV2)
|
||||
|
||||
// Write endpoints require authentication + org ownership (checked in handler)
|
||||
m.Group("", func() {
|
||||
m.Patch("", web.Bind(UpdateOrgV2Option{}), UpdateOrgV2)
|
||||
m.Put("/profile/readme", web.Bind(UpdateOrgProfileReadmeOption{}), UpdateOrgProfileReadmeV2)
|
||||
m.Post("/pinned", web.Bind(PinOrgRepoV2Option{}), PinOrgRepoV2)
|
||||
m.Delete("/pinned/{repo}", UnpinOrgRepoV2)
|
||||
}, reqToken())
|
||||
})
|
||||
|
||||
// AI settings API - org-scoped AI settings
|
||||
m.Group("/orgs/{org}/ai", func() {
|
||||
m.Get("/settings", GetOrgAISettingsV2)
|
||||
|
||||
@@ -685,9 +685,11 @@ func handleInitialize(ctx *context_service.APIContext, req *MCPRequest) {
|
||||
}
|
||||
|
||||
func handleToolsList(ctx *context_service.APIContext, req *MCPRequest) {
|
||||
allTools := make([]MCPTool, 0, len(mcpTools)+len(mcpAITools))
|
||||
allTools := make([]MCPTool, 0, len(mcpTools)+len(mcpAITools)+len(mcpPagesTools)+len(mcpOrgTools))
|
||||
allTools = append(allTools, mcpTools...)
|
||||
allTools = append(allTools, mcpAITools...)
|
||||
allTools = append(allTools, mcpPagesTools...)
|
||||
allTools = append(allTools, mcpOrgTools...)
|
||||
result := MCPToolsListResult{Tools: allTools}
|
||||
sendMCPResult(ctx, req.ID, result)
|
||||
}
|
||||
@@ -759,6 +761,52 @@ func handleToolsCall(ctx *context_service.APIContext, req *MCPRequest) {
|
||||
result, err = toolListIssues(ctx, params.Arguments)
|
||||
case "get_issue":
|
||||
result, err = toolGetIssue(ctx, params.Arguments)
|
||||
// Landing Pages tools
|
||||
case "get_landing_config":
|
||||
result, err = toolGetLandingConfig(ctx, params.Arguments)
|
||||
case "list_landing_templates":
|
||||
result, err = toolListLandingTemplates(ctx, params.Arguments)
|
||||
case "enable_landing_page":
|
||||
result, err = toolEnableLandingPage(ctx, params.Arguments)
|
||||
case "update_landing_brand":
|
||||
result, err = toolUpdateLandingBrand(ctx, params.Arguments)
|
||||
case "update_landing_hero":
|
||||
result, err = toolUpdateLandingHero(ctx, params.Arguments)
|
||||
case "update_landing_pricing":
|
||||
result, err = toolUpdateLandingPricing(ctx, params.Arguments)
|
||||
case "update_landing_comparison":
|
||||
result, err = toolUpdateLandingComparison(ctx, params.Arguments)
|
||||
case "update_landing_features":
|
||||
result, err = toolUpdateLandingFeatures(ctx, params.Arguments)
|
||||
case "update_landing_social_proof":
|
||||
result, err = toolUpdateLandingSocialProof(ctx, params.Arguments)
|
||||
case "update_landing_seo":
|
||||
result, err = toolUpdateLandingSEO(ctx, params.Arguments)
|
||||
case "update_landing_theme":
|
||||
result, err = toolUpdateLandingTheme(ctx, params.Arguments)
|
||||
case "update_landing_stats":
|
||||
result, err = toolUpdateLandingStats(ctx, params.Arguments)
|
||||
case "update_landing_value_props":
|
||||
result, err = toolUpdateLandingValueProps(ctx, params.Arguments)
|
||||
case "update_landing_cta":
|
||||
result, err = toolUpdateLandingCTA(ctx, params.Arguments)
|
||||
// Organization tools
|
||||
case "list_orgs":
|
||||
result, err = toolListOrgs(ctx, params.Arguments)
|
||||
case "get_org_overview":
|
||||
result, err = toolGetOrgOverview(ctx, params.Arguments)
|
||||
case "update_org":
|
||||
result, err = toolUpdateOrg(ctx, params.Arguments)
|
||||
case "list_org_repos":
|
||||
result, err = toolListOrgRepos(ctx, params.Arguments)
|
||||
case "get_org_profile_readme":
|
||||
result, err = toolGetOrgProfileReadme(ctx, params.Arguments)
|
||||
case "update_org_profile_readme":
|
||||
result, err = toolUpdateOrgProfileReadme(ctx, params.Arguments)
|
||||
case "pin_org_repo":
|
||||
result, err = toolPinOrgRepo(ctx, params.Arguments)
|
||||
case "unpin_org_repo":
|
||||
result, err = toolUnpinOrgRepo(ctx, params.Arguments)
|
||||
default:
|
||||
sendMCPError(ctx, req.ID, -32602, "Unknown tool", params.Name)
|
||||
return
|
||||
|
||||
720
routers/api/v2/mcp_org.go
Normal file
720
routers/api/v2/mcp_org.go
Normal file
@@ -0,0 +1,720 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
"code.gitcaddy.com/server/v3/models/organization"
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
"code.gitcaddy.com/server/v3/modules/optional"
|
||||
context_service "code.gitcaddy.com/server/v3/services/context"
|
||||
org_service "code.gitcaddy.com/server/v3/services/org"
|
||||
user_service "code.gitcaddy.com/server/v3/services/user"
|
||||
)
|
||||
|
||||
// mcpOrgTools defines MCP tools for organization management.
|
||||
var mcpOrgTools = []MCPTool{
|
||||
{
|
||||
Name: "list_orgs",
|
||||
Description: "List all organizations the authenticated user is a member of. Returns name, full name, description, avatar URL, website, location, group header, and visibility for each org.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_org_overview",
|
||||
Description: "Get a comprehensive overview of an organization including pinned repos, pinned groups, public members, stats (repos/members/teams/stars), profile README content, and recent repository activity.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"org": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Organization name",
|
||||
},
|
||||
},
|
||||
"required": []string{"org"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_org",
|
||||
Description: "Update an organization's basic information. Only provided fields are changed. Requires org owner or site admin.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"org": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Organization name",
|
||||
},
|
||||
"full_name": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Display name of the organization",
|
||||
},
|
||||
"email": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Contact email address",
|
||||
},
|
||||
"description": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Organization description",
|
||||
},
|
||||
"website": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Website URL",
|
||||
},
|
||||
"location": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Physical location",
|
||||
},
|
||||
"group_header": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Group header for organizing this org on the explore page",
|
||||
},
|
||||
},
|
||||
"required": []string{"org"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list_org_repos",
|
||||
Description: "List repositories for an organization. Supports grouping by group_header to see how repos are organized. Respects the caller's access permissions.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"org": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Organization name",
|
||||
},
|
||||
"group_by": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Group results by field. Use 'group_header' to see repos grouped by their assigned group.",
|
||||
"enum": []string{"group_header"},
|
||||
},
|
||||
"q": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Search keyword to filter repos by name",
|
||||
},
|
||||
"sort": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Sort order",
|
||||
"enum": []string{"newest", "oldest", "alphabetically", "reversealphabetically", "stars", "forks", "recentupdate"},
|
||||
},
|
||||
"limit": map[string]any{
|
||||
"type": "number",
|
||||
"description": "Number of repos to return (max 100, default 50)",
|
||||
},
|
||||
"page": map[string]any{
|
||||
"type": "number",
|
||||
"description": "Page number (1-based)",
|
||||
},
|
||||
},
|
||||
"required": []string{"org"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_org_profile_readme",
|
||||
Description: "Get the raw markdown content of an organization's profile README from its .profile repository.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"org": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Organization name",
|
||||
},
|
||||
},
|
||||
"required": []string{"org"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_org_profile_readme",
|
||||
Description: "Update (or create) the organization's profile README. Creates the .profile repository if it doesn't exist. Requires org owner or site admin.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"org": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Organization name",
|
||||
},
|
||||
"content": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Markdown content for the README",
|
||||
},
|
||||
"commit_message": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Git commit message (default: 'Update organization profile README')",
|
||||
},
|
||||
},
|
||||
"required": []string{"org", "content"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pin_org_repo",
|
||||
Description: "Pin a repository to the organization's overview page. Optionally assign it to a pinned group. Requires org owner or site admin.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"org": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Organization name",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name to pin",
|
||||
},
|
||||
"group_id": map[string]any{
|
||||
"type": "number",
|
||||
"description": "ID of the pinned group to add the repo to (0 or omit for ungrouped)",
|
||||
},
|
||||
"display_order": map[string]any{
|
||||
"type": "number",
|
||||
"description": "Display order within the group (lower = first)",
|
||||
},
|
||||
},
|
||||
"required": []string{"org", "repo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "unpin_org_repo",
|
||||
Description: "Unpin a repository from the organization's overview page. Requires org owner or site admin.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"org": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Organization name",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name to unpin",
|
||||
},
|
||||
},
|
||||
"required": []string{"org", "repo"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// --- Tool Handlers ---
|
||||
|
||||
func toolListOrgs(ctx *context_service.APIContext, _ map[string]any) (any, error) {
|
||||
if ctx.Doer == nil {
|
||||
return nil, errors.New("authentication required")
|
||||
}
|
||||
|
||||
opts := organization.FindOrgOptions{
|
||||
ListOptions: db.ListOptions{PageSize: 50},
|
||||
UserID: ctx.Doer.ID,
|
||||
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.Doer),
|
||||
}
|
||||
orgs, _, err := db.FindAndCount[organization.Organization](ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]map[string]any, 0, len(orgs))
|
||||
for _, org := range orgs {
|
||||
entry := map[string]any{
|
||||
"name": org.Name,
|
||||
"full_name": org.FullName,
|
||||
"description": org.Description,
|
||||
"avatar_url": org.AsUser().AvatarLink(ctx),
|
||||
"website": org.Website,
|
||||
"location": org.Location,
|
||||
"group_header": org.GroupHeader,
|
||||
"visibility": org.Visibility.String(),
|
||||
}
|
||||
result = append(result, entry)
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"organizations": result,
|
||||
"count": len(result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetOrgOverview(ctx *context_service.APIContext, args map[string]any) (any, error) {
|
||||
orgName, _ := args["org"].(string)
|
||||
if orgName == "" {
|
||||
return nil, errors.New("org is required")
|
||||
}
|
||||
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("organization not found: %s", orgName)
|
||||
}
|
||||
|
||||
// Stats
|
||||
stats, err := org_service.GetOrgOverviewStats(ctx, org.ID, ctx.Doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pinned repos
|
||||
pinnedRepos, err := org_service.GetOrgPinnedReposWithDetails(ctx, org.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiPinned := make([]map[string]any, 0, len(pinnedRepos))
|
||||
for _, p := range pinnedRepos {
|
||||
if p.Repo == nil {
|
||||
continue
|
||||
}
|
||||
entry := map[string]any{
|
||||
"id": p.ID,
|
||||
"display_order": p.DisplayOrder,
|
||||
}
|
||||
if repo, ok := p.Repo.(*repo_model.Repository); ok {
|
||||
entry["repo_name"] = repo.Name
|
||||
entry["repo_full_name"] = repo.FullName()
|
||||
entry["description"] = repo.Description
|
||||
entry["is_private"] = repo.IsPrivate
|
||||
}
|
||||
if p.Group != nil {
|
||||
entry["group_id"] = p.GroupID
|
||||
entry["group_name"] = p.Group.Name
|
||||
}
|
||||
apiPinned = append(apiPinned, entry)
|
||||
}
|
||||
|
||||
// Pinned groups
|
||||
pinnedGroups, err := organization.GetOrgPinnedGroups(ctx, org.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiGroups := make([]map[string]any, 0, len(pinnedGroups))
|
||||
for _, g := range pinnedGroups {
|
||||
apiGroups = append(apiGroups, map[string]any{
|
||||
"id": g.ID,
|
||||
"name": g.Name,
|
||||
"display_order": g.DisplayOrder,
|
||||
"collapsed": g.Collapsed,
|
||||
})
|
||||
}
|
||||
|
||||
// Profile readme
|
||||
readme, _ := org_service.GetOrgProfileReadme(ctx, org.ID)
|
||||
|
||||
// Recent activity
|
||||
recentActivity, _ := org_service.GetOrgRecentActivity(ctx, org.ID, ctx.Doer, 10)
|
||||
apiRecent := make([]map[string]any, 0, len(recentActivity))
|
||||
for _, a := range recentActivity {
|
||||
apiRecent = append(apiRecent, map[string]any{
|
||||
"repo_name": a.RepoName,
|
||||
"repo_full_name": a.RepoFullName,
|
||||
"commit_message": a.CommitMessage,
|
||||
"commit_time": a.CommitTime,
|
||||
"is_private": a.IsPrivate,
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"organization": map[string]any{
|
||||
"name": org.Name,
|
||||
"full_name": org.FullName,
|
||||
"description": org.Description,
|
||||
"website": org.Website,
|
||||
"location": org.Location,
|
||||
"group_header": org.GroupHeader,
|
||||
"visibility": org.Visibility.String(),
|
||||
},
|
||||
"stats": map[string]any{
|
||||
"total_repos": stats.TotalRepos,
|
||||
"total_members": stats.TotalMembers,
|
||||
"total_teams": stats.TotalTeams,
|
||||
"total_stars": stats.TotalStars,
|
||||
},
|
||||
"pinned_repos": apiPinned,
|
||||
"pinned_groups": apiGroups,
|
||||
"profile_readme": readme,
|
||||
"recent_activity": apiRecent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolUpdateOrg(ctx *context_service.APIContext, args map[string]any) (any, error) {
|
||||
orgName, _ := args["org"].(string)
|
||||
if orgName == "" {
|
||||
return nil, errors.New("org is required")
|
||||
}
|
||||
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("organization not found: %s", orgName)
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ctx.Doer == nil {
|
||||
return nil, errors.New("authentication required")
|
||||
}
|
||||
if !ctx.Doer.IsAdmin {
|
||||
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isOwner {
|
||||
return nil, errors.New("organization admin access required")
|
||||
}
|
||||
}
|
||||
|
||||
opts := &user_service.UpdateOptions{}
|
||||
updated := []string{}
|
||||
|
||||
if v, ok := args["full_name"].(string); ok {
|
||||
opts.FullName = optional.Some(v)
|
||||
updated = append(updated, "full_name")
|
||||
}
|
||||
if v, ok := args["description"].(string); ok {
|
||||
opts.Description = optional.Some(v)
|
||||
updated = append(updated, "description")
|
||||
}
|
||||
if v, ok := args["website"].(string); ok {
|
||||
opts.Website = optional.Some(v)
|
||||
updated = append(updated, "website")
|
||||
}
|
||||
if v, ok := args["location"].(string); ok {
|
||||
opts.Location = optional.Some(v)
|
||||
updated = append(updated, "location")
|
||||
}
|
||||
if v, ok := args["group_header"].(string); ok {
|
||||
opts.GroupHeader = optional.Some(v)
|
||||
updated = append(updated, "group_header")
|
||||
}
|
||||
|
||||
if v, ok := args["email"].(string); ok && v != "" {
|
||||
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), v); err != nil {
|
||||
return nil, fmt.Errorf("failed to update email: %v", err)
|
||||
}
|
||||
updated = append(updated, "email")
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"status": "updated",
|
||||
"org": orgName,
|
||||
"fields_updated": updated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolListOrgRepos(ctx *context_service.APIContext, args map[string]any) (any, error) {
|
||||
orgName, _ := args["org"].(string)
|
||||
if orgName == "" {
|
||||
return nil, errors.New("org is required")
|
||||
}
|
||||
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("organization not found: %s", orgName)
|
||||
}
|
||||
|
||||
limit := 50
|
||||
if v, ok := args["limit"].(float64); ok && v > 0 {
|
||||
limit = min(int(v), 100)
|
||||
}
|
||||
page := 1
|
||||
if v, ok := args["page"].(float64); ok && v > 0 {
|
||||
page = int(v)
|
||||
}
|
||||
|
||||
groupBy, _ := args["group_by"].(string)
|
||||
keyword, _ := args["q"].(string)
|
||||
sortOrder, _ := args["sort"].(string)
|
||||
|
||||
var orderBy db.SearchOrderBy
|
||||
switch sortOrder {
|
||||
case "newest":
|
||||
orderBy = db.SearchOrderByNewest
|
||||
case "oldest":
|
||||
orderBy = db.SearchOrderByOldest
|
||||
case "reversealphabetically":
|
||||
orderBy = db.SearchOrderByAlphabeticallyReverse
|
||||
case "stars":
|
||||
orderBy = db.SearchOrderByStarsReverse
|
||||
case "forks":
|
||||
orderBy = db.SearchOrderByForksReverse
|
||||
case "recentupdate":
|
||||
orderBy = db.SearchOrderByRecentUpdated
|
||||
default:
|
||||
orderBy = db.SearchOrderByAlphabetically
|
||||
}
|
||||
|
||||
if groupBy == "group_header" {
|
||||
orderBy = db.SearchOrderBy("CASE WHEN group_header = '' OR group_header IS NULL THEN 1 ELSE 0 END, group_header ASC, " + string(orderBy))
|
||||
}
|
||||
|
||||
repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{PageSize: limit, Page: page},
|
||||
Keyword: keyword,
|
||||
OwnerID: org.ID,
|
||||
OrderBy: orderBy,
|
||||
Private: ctx.Doer != nil,
|
||||
Actor: ctx.Doer,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if groupBy == "group_header" {
|
||||
// Grouped response
|
||||
type repoEntry struct {
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Description string `json:"description"`
|
||||
IsPrivate bool `json:"is_private"`
|
||||
IsFork bool `json:"is_fork"`
|
||||
IsArchived bool `json:"is_archived"`
|
||||
Stars int `json:"stars"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
grouped := make(map[string][]repoEntry)
|
||||
var headers []string
|
||||
headerSeen := make(map[string]bool)
|
||||
|
||||
for _, repo := range repos {
|
||||
header := repo.GroupHeader
|
||||
if !headerSeen[header] {
|
||||
headerSeen[header] = true
|
||||
headers = append(headers, header)
|
||||
}
|
||||
grouped[header] = append(grouped[header], repoEntry{
|
||||
Name: repo.Name,
|
||||
FullName: repo.FullName(),
|
||||
Description: repo.Description,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
IsFork: repo.IsFork,
|
||||
IsArchived: repo.IsArchived,
|
||||
Stars: repo.NumStars,
|
||||
Language: repoLanguage(repo),
|
||||
})
|
||||
}
|
||||
|
||||
groups := make([]map[string]any, 0, len(headers))
|
||||
for _, h := range headers {
|
||||
displayName := h
|
||||
if displayName == "" {
|
||||
displayName = "(ungrouped)"
|
||||
}
|
||||
groups = append(groups, map[string]any{
|
||||
"group_header": h,
|
||||
"display_name": displayName,
|
||||
"repos": grouped[h],
|
||||
"count": len(grouped[h]),
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"groups": groups,
|
||||
"total": count,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Flat response
|
||||
repoList := make([]map[string]any, 0, len(repos))
|
||||
for _, repo := range repos {
|
||||
repoList = append(repoList, map[string]any{
|
||||
"name": repo.Name,
|
||||
"full_name": repo.FullName(),
|
||||
"description": repo.Description,
|
||||
"is_private": repo.IsPrivate,
|
||||
"is_fork": repo.IsFork,
|
||||
"is_archived": repo.IsArchived,
|
||||
"stars": repo.NumStars,
|
||||
"language": repoLanguage(repo),
|
||||
"group_header": repo.GroupHeader,
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"repos": repoList,
|
||||
"total": count,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetOrgProfileReadme(ctx *context_service.APIContext, args map[string]any) (any, error) {
|
||||
orgName, _ := args["org"].(string)
|
||||
if orgName == "" {
|
||||
return nil, errors.New("org is required")
|
||||
}
|
||||
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("organization not found: %s", orgName)
|
||||
}
|
||||
|
||||
readme, err := org_service.GetOrgProfileReadme(ctx, org.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"org": orgName,
|
||||
"content": readme,
|
||||
"has_profile": readme != "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolUpdateOrgProfileReadme(ctx *context_service.APIContext, args map[string]any) (any, error) {
|
||||
orgName, _ := args["org"].(string)
|
||||
if orgName == "" {
|
||||
return nil, errors.New("org is required")
|
||||
}
|
||||
content, _ := args["content"].(string)
|
||||
if content == "" {
|
||||
return nil, errors.New("content is required")
|
||||
}
|
||||
commitMessage, _ := args["commit_message"].(string)
|
||||
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("organization not found: %s", orgName)
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ctx.Doer == nil {
|
||||
return nil, errors.New("authentication required")
|
||||
}
|
||||
if !ctx.Doer.IsAdmin {
|
||||
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isOwner {
|
||||
return nil, errors.New("organization admin access required")
|
||||
}
|
||||
}
|
||||
|
||||
if err := org_service.UpdateOrgProfileReadme(ctx, ctx.Doer, org.ID, content, commitMessage); err != nil {
|
||||
return nil, fmt.Errorf("failed to update profile readme: %v", err)
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"status": "updated",
|
||||
"org": orgName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolPinOrgRepo(ctx *context_service.APIContext, args map[string]any) (any, error) {
|
||||
orgName, _ := args["org"].(string)
|
||||
repoName, _ := args["repo"].(string)
|
||||
if orgName == "" || repoName == "" {
|
||||
return nil, errors.New("org and repo are required")
|
||||
}
|
||||
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("organization not found: %s", orgName)
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ctx.Doer == nil {
|
||||
return nil, errors.New("authentication required")
|
||||
}
|
||||
if !ctx.Doer.IsAdmin {
|
||||
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isOwner {
|
||||
return nil, errors.New("organization admin access required")
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := repo_model.GetRepositoryByName(ctx, org.ID, repoName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", orgName, repoName)
|
||||
}
|
||||
|
||||
isPinned, err := organization.IsRepoPinned(ctx, org.ID, repo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isPinned {
|
||||
return map[string]any{
|
||||
"status": "already_pinned",
|
||||
"org": orgName,
|
||||
"repo": repoName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var groupID int64
|
||||
if v, ok := args["group_id"].(float64); ok {
|
||||
groupID = int64(v)
|
||||
}
|
||||
var displayOrder int
|
||||
if v, ok := args["display_order"].(float64); ok {
|
||||
displayOrder = int(v)
|
||||
}
|
||||
|
||||
pinned := &organization.OrgPinnedRepo{
|
||||
OrgID: org.ID,
|
||||
RepoID: repo.ID,
|
||||
GroupID: groupID,
|
||||
DisplayOrder: displayOrder,
|
||||
}
|
||||
|
||||
if err := organization.CreateOrgPinnedRepo(ctx, pinned); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"status": "pinned",
|
||||
"org": orgName,
|
||||
"repo": repoName,
|
||||
"id": pinned.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolUnpinOrgRepo(ctx *context_service.APIContext, args map[string]any) (any, error) {
|
||||
orgName, _ := args["org"].(string)
|
||||
repoName, _ := args["repo"].(string)
|
||||
if orgName == "" || repoName == "" {
|
||||
return nil, errors.New("org and repo are required")
|
||||
}
|
||||
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("organization not found: %s", orgName)
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ctx.Doer == nil {
|
||||
return nil, errors.New("authentication required")
|
||||
}
|
||||
if !ctx.Doer.IsAdmin {
|
||||
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isOwner {
|
||||
return nil, errors.New("organization admin access required")
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := repo_model.GetRepositoryByName(ctx, org.ID, repoName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", orgName, repoName)
|
||||
}
|
||||
|
||||
if err := organization.DeleteOrgPinnedRepo(ctx, org.ID, repo.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"status": "unpinned",
|
||||
"org": orgName,
|
||||
"repo": repoName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func repoLanguage(repo *repo_model.Repository) string {
|
||||
if repo.PrimaryLanguage != nil {
|
||||
return repo.PrimaryLanguage.Language
|
||||
}
|
||||
return ""
|
||||
}
|
||||
794
routers/api/v2/mcp_pages.go
Normal file
794
routers/api/v2/mcp_pages.go
Normal file
@@ -0,0 +1,794 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
user_model "code.gitcaddy.com/server/v3/models/user"
|
||||
"code.gitcaddy.com/server/v3/modules/json"
|
||||
pages_module "code.gitcaddy.com/server/v3/modules/pages"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
pages_service "code.gitcaddy.com/server/v3/services/pages"
|
||||
)
|
||||
|
||||
// Landing Pages MCP Tools
|
||||
var mcpPagesTools = []MCPTool{
|
||||
{
|
||||
Name: "get_landing_config",
|
||||
Description: "Get the full landing page configuration for a repository. Returns all sections: brand, hero, pricing, comparison, features, social proof, SEO, and more.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list_landing_templates",
|
||||
Description: "List available landing page templates with display names. Templates include: open-source-hero, saas-conversion, bold-marketing, developer-tool, and more.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "enable_landing_page",
|
||||
Description: "Enable or disable the landing page for a repository. Optionally set a template.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo", "enabled"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"enabled": map[string]any{"type": "boolean", "description": "Enable or disable"},
|
||||
"template": map[string]any{"type": "string", "description": "Template name (e.g., 'saas-conversion', 'open-source-hero')"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_brand",
|
||||
Description: "Update the brand section of a landing page: name, logo URL, tagline, favicon.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"name": map[string]any{"type": "string", "description": "Brand name"},
|
||||
"logo_url": map[string]any{"type": "string", "description": "Logo image URL"},
|
||||
"tagline": map[string]any{"type": "string", "description": "Brand tagline"},
|
||||
"favicon_url": map[string]any{"type": "string", "description": "Favicon URL"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_hero",
|
||||
Description: "Update the hero section: headline, subheadline, CTA buttons, hero image or video URL.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"headline": map[string]any{"type": "string", "description": "Main headline"},
|
||||
"subheadline": map[string]any{"type": "string", "description": "Supporting text"},
|
||||
"image_url": map[string]any{"type": "string", "description": "Hero image URL"},
|
||||
"video_url": map[string]any{"type": "string", "description": "Hero video URL"},
|
||||
"primary_cta_label": map[string]any{"type": "string", "description": "Primary CTA button label"},
|
||||
"primary_cta_url": map[string]any{"type": "string", "description": "Primary CTA button URL"},
|
||||
"secondary_cta_label": map[string]any{"type": "string", "description": "Secondary CTA button label"},
|
||||
"secondary_cta_url": map[string]any{"type": "string", "description": "Secondary CTA button URL"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_pricing",
|
||||
Description: "Update the pricing section with plans. Each plan has a name, price, period, feature list, CTA, and optional 'featured' flag.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo", "plans"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"headline": map[string]any{"type": "string", "description": "Pricing section headline"},
|
||||
"subheadline": map[string]any{"type": "string", "description": "Pricing section subheadline"},
|
||||
"plans": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"name": map[string]any{"type": "string"},
|
||||
"price": map[string]any{"type": "string"},
|
||||
"period": map[string]any{"type": "string"},
|
||||
"features": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
||||
"cta": map[string]any{"type": "string"},
|
||||
"featured": map[string]any{"type": "boolean"},
|
||||
},
|
||||
},
|
||||
"description": "Array of pricing plans",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_comparison",
|
||||
Description: "Update the feature comparison matrix. Define columns (products/tiers) and groups of features with per-column values.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"enabled": map[string]any{"type": "boolean", "description": "Enable comparison section"},
|
||||
"headline": map[string]any{"type": "string", "description": "Section headline"},
|
||||
"subheadline": map[string]any{"type": "string", "description": "Section subheadline"},
|
||||
"columns": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{"type": "string"},
|
||||
"description": "Column headers (e.g., ['Free', 'Pro', 'Enterprise'])",
|
||||
},
|
||||
"groups": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"name": map[string]any{"type": "string"},
|
||||
"features": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"name": map[string]any{"type": "string"},
|
||||
"values": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "Feature groups with per-column values",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_features",
|
||||
Description: "Update the features section with title, description, and optional icon/image for each feature.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo", "features"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"features": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"title": map[string]any{"type": "string"},
|
||||
"description": map[string]any{"type": "string"},
|
||||
"icon": map[string]any{"type": "string"},
|
||||
"image_url": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
"description": "Array of features",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_social_proof",
|
||||
Description: "Update testimonials and client logos for social proof.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"logos": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{"type": "string"},
|
||||
"description": "Client/partner logo URLs",
|
||||
},
|
||||
"testimonials": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"quote": map[string]any{"type": "string"},
|
||||
"author": map[string]any{"type": "string"},
|
||||
"role": map[string]any{"type": "string"},
|
||||
"avatar": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
"description": "Testimonials",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_seo",
|
||||
Description: "Update SEO metadata: title, description, keywords, Open Graph image, Twitter card settings.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"title": map[string]any{"type": "string", "description": "SEO title"},
|
||||
"description": map[string]any{"type": "string", "description": "SEO description"},
|
||||
"keywords": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "SEO keywords"},
|
||||
"og_image": map[string]any{"type": "string", "description": "Open Graph image URL"},
|
||||
"twitter_card": map[string]any{"type": "string", "description": "Twitter card type (summary, summary_large_image)"},
|
||||
"twitter_site": map[string]any{"type": "string", "description": "Twitter @handle"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_theme",
|
||||
Description: "Update the visual theme: primary color, accent color, light/dark/auto mode.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"primary_color": map[string]any{"type": "string", "description": "Primary brand color (hex, e.g., '#512BD4')"},
|
||||
"accent_color": map[string]any{"type": "string", "description": "Accent color (hex)"},
|
||||
"mode": map[string]any{"type": "string", "enum": []string{"light", "dark", "auto"}, "description": "Color mode"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_stats",
|
||||
Description: "Update the stats counters displayed on the landing page. Each stat has a value and label.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo", "stats"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"stats": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"value": map[string]any{"type": "string"},
|
||||
"label": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
"description": "Array of stat counters (e.g., [{value: '15+', label: 'Tools'}])",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_value_props",
|
||||
Description: "Update the value propositions section. Each value prop has a title, description, and icon.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo", "value_props"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"value_props": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"title": map[string]any{"type": "string"},
|
||||
"description": map[string]any{"type": "string"},
|
||||
"icon": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
"description": "Array of value propositions",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_cta",
|
||||
Description: "Update the call-to-action section at the bottom of the page with headline, subheadline, and button.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"headline": map[string]any{"type": "string", "description": "CTA headline"},
|
||||
"subheadline": map[string]any{"type": "string", "description": "CTA subheadline"},
|
||||
"button_label": map[string]any{"type": "string", "description": "Button text"},
|
||||
"button_url": map[string]any{"type": "string", "description": "Button URL"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ── Tool Implementations ──────────────────────────────────
|
||||
|
||||
func toolGetLandingConfig(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
owner, repo, err := resolveOwnerRepo(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoObj, err := getRepoByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := pages_service.GetPagesConfig(ctx, repoObj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get pages config: %w", err)
|
||||
}
|
||||
if config == nil {
|
||||
return map[string]any{"enabled": false, "message": "No landing page configured"}, nil
|
||||
}
|
||||
|
||||
return buildFullResponse(config), nil
|
||||
}
|
||||
|
||||
func toolListLandingTemplates(_ *context.APIContext, _ map[string]any) (any, error) { //nolint:unparam // signature must match tool handler type
|
||||
templates := pages_module.ValidTemplates()
|
||||
displayNames := pages_module.TemplateDisplayNames()
|
||||
|
||||
result := make([]map[string]string, 0, len(templates))
|
||||
for _, t := range templates {
|
||||
result = append(result, map[string]string{
|
||||
"id": t,
|
||||
"name": displayNames[t],
|
||||
})
|
||||
}
|
||||
return map[string]any{"templates": result}, nil
|
||||
}
|
||||
|
||||
func toolEnableLandingPage(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
owner, repo, err := resolveOwnerRepo(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoObj, err := getRepoByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enabled, _ := args["enabled"].(bool)
|
||||
template, _ := args["template"].(string)
|
||||
|
||||
if enabled {
|
||||
if template == "" {
|
||||
template = "open-source-hero"
|
||||
}
|
||||
if !pages_module.IsValidTemplate(template) {
|
||||
return nil, fmt.Errorf("invalid template: %s", template)
|
||||
}
|
||||
if err := pages_service.EnablePages(ctx, repoObj, template); err != nil {
|
||||
return nil, fmt.Errorf("enable pages: %w", err)
|
||||
}
|
||||
return map[string]any{"enabled": true, "template": template}, nil
|
||||
}
|
||||
|
||||
if err := pages_service.DisablePages(ctx, repoObj); err != nil {
|
||||
return nil, fmt.Errorf("disable pages: %w", err)
|
||||
}
|
||||
return map[string]any{"enabled": false}, nil
|
||||
}
|
||||
|
||||
func toolUpdateLandingBrand(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["name"].(string); ok {
|
||||
config.Brand.Name = v
|
||||
}
|
||||
if v, ok := args["logo_url"].(string); ok {
|
||||
config.Brand.LogoURL = v
|
||||
}
|
||||
if v, ok := args["tagline"].(string); ok {
|
||||
config.Brand.Tagline = v
|
||||
}
|
||||
if v, ok := args["favicon_url"].(string); ok {
|
||||
config.Brand.FaviconURL = v
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "brand")
|
||||
}
|
||||
|
||||
func toolUpdateLandingHero(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["headline"].(string); ok {
|
||||
config.Hero.Headline = v
|
||||
}
|
||||
if v, ok := args["subheadline"].(string); ok {
|
||||
config.Hero.Subheadline = v
|
||||
}
|
||||
if v, ok := args["image_url"].(string); ok {
|
||||
config.Hero.ImageURL = v
|
||||
}
|
||||
if v, ok := args["video_url"].(string); ok {
|
||||
config.Hero.VideoURL = v
|
||||
}
|
||||
if v, ok := args["primary_cta_label"].(string); ok {
|
||||
config.Hero.PrimaryCTA.Label = v
|
||||
}
|
||||
if v, ok := args["primary_cta_url"].(string); ok {
|
||||
config.Hero.PrimaryCTA.URL = v
|
||||
}
|
||||
if v, ok := args["secondary_cta_label"].(string); ok {
|
||||
config.Hero.SecondaryCTA.Label = v
|
||||
}
|
||||
if v, ok := args["secondary_cta_url"].(string); ok {
|
||||
config.Hero.SecondaryCTA.URL = v
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "hero")
|
||||
}
|
||||
|
||||
func toolUpdateLandingPricing(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["headline"].(string); ok {
|
||||
config.Pricing.Headline = v
|
||||
}
|
||||
if v, ok := args["subheadline"].(string); ok {
|
||||
config.Pricing.Subheadline = v
|
||||
}
|
||||
if plans, ok := args["plans"].([]any); ok {
|
||||
config.Pricing.Plans = nil
|
||||
for _, p := range plans {
|
||||
if pm, ok := p.(map[string]any); ok {
|
||||
plan := pages_module.PricingPlanConfig{
|
||||
Name: strVal(pm, "name"),
|
||||
Price: strVal(pm, "price"),
|
||||
Period: strVal(pm, "period"),
|
||||
CTA: strVal(pm, "cta"),
|
||||
Featured: boolVal(pm, "featured"),
|
||||
}
|
||||
if features, ok := pm["features"].([]any); ok {
|
||||
for _, f := range features {
|
||||
if s, ok := f.(string); ok {
|
||||
plan.Features = append(plan.Features, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
config.Pricing.Plans = append(config.Pricing.Plans, plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "pricing")
|
||||
}
|
||||
|
||||
func toolUpdateLandingComparison(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["enabled"].(bool); ok {
|
||||
config.Comparison.Enabled = v
|
||||
}
|
||||
if v, ok := args["headline"].(string); ok {
|
||||
config.Comparison.Headline = v
|
||||
}
|
||||
if v, ok := args["subheadline"].(string); ok {
|
||||
config.Comparison.Subheadline = v
|
||||
}
|
||||
if cols, ok := args["columns"].([]any); ok {
|
||||
config.Comparison.Columns = nil
|
||||
for _, c := range cols {
|
||||
if s, ok := c.(string); ok {
|
||||
config.Comparison.Columns = append(config.Comparison.Columns, pages_module.ComparisonColumnConfig{Name: s})
|
||||
} else if cm, ok := c.(map[string]any); ok {
|
||||
config.Comparison.Columns = append(config.Comparison.Columns, pages_module.ComparisonColumnConfig{
|
||||
Name: strVal(cm, "name"),
|
||||
Highlight: boolVal(cm, "highlight"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if groups, ok := args["groups"].([]any); ok {
|
||||
config.Comparison.Groups = nil
|
||||
for _, g := range groups {
|
||||
if gm, ok := g.(map[string]any); ok {
|
||||
group := pages_module.ComparisonGroupConfig{Name: strVal(gm, "name")}
|
||||
if features, ok := gm["features"].([]any); ok {
|
||||
for _, f := range features {
|
||||
if fm, ok := f.(map[string]any); ok {
|
||||
feature := pages_module.ComparisonFeatureConfig{Name: strVal(fm, "name")}
|
||||
if vals, ok := fm["values"].([]any); ok {
|
||||
for _, v := range vals {
|
||||
if s, ok := v.(string); ok {
|
||||
feature.Values = append(feature.Values, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
group.Features = append(group.Features, feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
config.Comparison.Groups = append(config.Comparison.Groups, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "comparison")
|
||||
}
|
||||
|
||||
func toolUpdateLandingFeatures(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if features, ok := args["features"].([]any); ok {
|
||||
config.Features = nil
|
||||
for _, f := range features {
|
||||
if fm, ok := f.(map[string]any); ok {
|
||||
config.Features = append(config.Features, pages_module.FeatureConfig{
|
||||
Title: strVal(fm, "title"),
|
||||
Description: strVal(fm, "description"),
|
||||
Icon: strVal(fm, "icon"),
|
||||
ImageURL: strVal(fm, "image_url"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "features")
|
||||
}
|
||||
|
||||
func toolUpdateLandingSocialProof(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if logos, ok := args["logos"].([]any); ok {
|
||||
config.SocialProof.Logos = nil
|
||||
for _, l := range logos {
|
||||
if s, ok := l.(string); ok {
|
||||
config.SocialProof.Logos = append(config.SocialProof.Logos, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if testimonials, ok := args["testimonials"].([]any); ok {
|
||||
config.SocialProof.Testimonials = nil
|
||||
for _, t := range testimonials {
|
||||
if tm, ok := t.(map[string]any); ok {
|
||||
config.SocialProof.Testimonials = append(config.SocialProof.Testimonials, pages_module.TestimonialConfig{
|
||||
Quote: strVal(tm, "quote"),
|
||||
Author: strVal(tm, "author"),
|
||||
Role: strVal(tm, "role"),
|
||||
Avatar: strVal(tm, "avatar"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "social_proof")
|
||||
}
|
||||
|
||||
func toolUpdateLandingSEO(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["title"].(string); ok {
|
||||
config.SEO.Title = v
|
||||
}
|
||||
if v, ok := args["description"].(string); ok {
|
||||
config.SEO.Description = v
|
||||
}
|
||||
if v, ok := args["og_image"].(string); ok {
|
||||
config.SEO.OGImage = v
|
||||
}
|
||||
if v, ok := args["twitter_card"].(string); ok {
|
||||
config.SEO.TwitterCard = v
|
||||
}
|
||||
if v, ok := args["twitter_site"].(string); ok {
|
||||
config.SEO.TwitterSite = v
|
||||
}
|
||||
if keywords, ok := args["keywords"].([]any); ok {
|
||||
config.SEO.Keywords = nil
|
||||
for _, k := range keywords {
|
||||
if s, ok := k.(string); ok {
|
||||
config.SEO.Keywords = append(config.SEO.Keywords, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "seo")
|
||||
}
|
||||
|
||||
func toolUpdateLandingTheme(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["primary_color"].(string); ok {
|
||||
config.Theme.PrimaryColor = v
|
||||
}
|
||||
if v, ok := args["accent_color"].(string); ok {
|
||||
config.Theme.AccentColor = v
|
||||
}
|
||||
if v, ok := args["mode"].(string); ok {
|
||||
config.Theme.Mode = v
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "theme")
|
||||
}
|
||||
|
||||
func toolUpdateLandingStats(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stats, ok := args["stats"].([]any); ok {
|
||||
config.Stats = nil
|
||||
for _, s := range stats {
|
||||
if sm, ok := s.(map[string]any); ok {
|
||||
config.Stats = append(config.Stats, pages_module.StatConfig{
|
||||
Value: strVal(sm, "value"),
|
||||
Label: strVal(sm, "label"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "stats")
|
||||
}
|
||||
|
||||
func toolUpdateLandingValueProps(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if vps, ok := args["value_props"].([]any); ok {
|
||||
config.ValueProps = nil
|
||||
for _, v := range vps {
|
||||
if vm, ok := v.(map[string]any); ok {
|
||||
config.ValueProps = append(config.ValueProps, pages_module.ValuePropConfig{
|
||||
Title: strVal(vm, "title"),
|
||||
Description: strVal(vm, "description"),
|
||||
Icon: strVal(vm, "icon"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "value_props")
|
||||
}
|
||||
|
||||
func toolUpdateLandingCTA(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["headline"].(string); ok {
|
||||
config.CTASection.Headline = v
|
||||
}
|
||||
if v, ok := args["subheadline"].(string); ok {
|
||||
config.CTASection.Subheadline = v
|
||||
}
|
||||
if v, ok := args["button_label"].(string); ok {
|
||||
config.CTASection.Button.Label = v
|
||||
}
|
||||
if v, ok := args["button_url"].(string); ok {
|
||||
config.CTASection.Button.URL = v
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "cta_section")
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────
|
||||
|
||||
func getConfigForUpdate(ctx *context.APIContext, args map[string]any) (*pages_module.LandingConfig, *repo_model.Repository, error) {
|
||||
owner, repo, err := resolveOwnerRepo(args)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
repoObj, err := getRepoByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
config, err := pages_service.GetPagesConfig(ctx, repoObj)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("get config: %w", err)
|
||||
}
|
||||
if config == nil {
|
||||
config = pages_module.DefaultConfig()
|
||||
}
|
||||
|
||||
return config, repoObj, nil
|
||||
}
|
||||
|
||||
func saveAndReturn(ctx *context.APIContext, repo *repo_model.Repository, config *pages_module.LandingConfig, section string) (any, error) {
|
||||
configJSON, _ := json.Marshal(config)
|
||||
hash := pages_module.HashConfig(configJSON)
|
||||
|
||||
existing, _ := repo_model.GetPagesConfigByRepoID(ctx, repo.ID)
|
||||
|
||||
if existing != nil {
|
||||
existing.ConfigJSON = string(configJSON)
|
||||
existing.ConfigHash = hash
|
||||
existing.Template = repo_model.PagesTemplate(config.Template)
|
||||
existing.Enabled = config.Enabled
|
||||
if err := repo_model.UpdatePagesConfig(ctx, existing); err != nil {
|
||||
return nil, fmt.Errorf("save config: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := repo_model.CreatePagesConfig(ctx, &repo_model.PagesConfig{
|
||||
RepoID: repo.ID,
|
||||
Enabled: config.Enabled,
|
||||
Template: repo_model.PagesTemplate(config.Template),
|
||||
ConfigJSON: string(configJSON),
|
||||
ConfigHash: hash,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("create config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"section": section,
|
||||
"message": fmt.Sprintf("Updated %s section", section),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func strVal(m map[string]any, key string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func boolVal(m map[string]any, key string) bool {
|
||||
if v, ok := m[key].(bool); ok {
|
||||
return v
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func resolveOwnerRepo(args map[string]any) (string, string, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
if owner == "" || repo == "" {
|
||||
return "", "", errors.New("owner and repo are required")
|
||||
}
|
||||
return owner, repo, nil
|
||||
}
|
||||
|
||||
func getRepoByOwnerAndName(ctx *context.APIContext, owner, repo string) (*repo_model.Repository, error) {
|
||||
ownerObj, err := user_model.GetUserByName(ctx, owner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("owner not found: %s", owner)
|
||||
}
|
||||
repoObj, err := repo_model.GetRepositoryByName(ctx, ownerObj.ID, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repo not found: %s/%s", owner, repo)
|
||||
}
|
||||
return repoObj, nil
|
||||
}
|
||||
526
routers/api/v2/org_overview.go
Normal file
526
routers/api/v2/org_overview.go
Normal file
@@ -0,0 +1,526 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
"code.gitcaddy.com/server/v3/models/organization"
|
||||
"code.gitcaddy.com/server/v3/models/perm"
|
||||
access_model "code.gitcaddy.com/server/v3/models/perm/access"
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
apierrors "code.gitcaddy.com/server/v3/modules/errors"
|
||||
"code.gitcaddy.com/server/v3/modules/optional"
|
||||
api "code.gitcaddy.com/server/v3/modules/structs"
|
||||
"code.gitcaddy.com/server/v3/modules/web"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
"code.gitcaddy.com/server/v3/services/convert"
|
||||
org_service "code.gitcaddy.com/server/v3/services/org"
|
||||
user_service "code.gitcaddy.com/server/v3/services/user"
|
||||
)
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
// loadOrg looks up the org from the path param and checks it exists.
|
||||
func loadOrg(ctx *context.APIContext) *organization.Organization {
|
||||
orgName := ctx.PathParam("org")
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
ctx.APIErrorWithCode(apierrors.OrgNotFound)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return org
|
||||
}
|
||||
|
||||
// requireOrgOwner checks the doer is an org owner or site admin. Returns false if denied.
|
||||
func requireOrgOwner(ctx *context.APIContext, org *organization.Organization) bool {
|
||||
if ctx.Doer.IsAdmin {
|
||||
return true
|
||||
}
|
||||
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return false
|
||||
}
|
||||
if !isOwner {
|
||||
ctx.APIErrorWithCode(apierrors.PermOrgAdminRequired)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// --- List user's orgs ---
|
||||
|
||||
// ListUserOrgsV2 returns all organizations the authenticated user belongs to.
|
||||
func ListUserOrgsV2(ctx *context.APIContext) {
|
||||
opts := organization.FindOrgOptions{
|
||||
ListOptions: db.ListOptions{PageSize: 50},
|
||||
UserID: ctx.Doer.ID,
|
||||
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.Doer),
|
||||
}
|
||||
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiOrgs := make([]*api.Organization, len(orgs))
|
||||
for i := range orgs {
|
||||
apiOrgs[i] = convert.ToOrganization(ctx, orgs[i])
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(maxResults)
|
||||
ctx.JSON(http.StatusOK, apiOrgs)
|
||||
}
|
||||
|
||||
// --- Org Overview ---
|
||||
|
||||
// GetOrgOverviewV2 returns the full organization overview including profile readme and recent activity.
|
||||
func GetOrgOverviewV2(ctx *context.APIContext) {
|
||||
org := loadOrg(ctx)
|
||||
if org == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Pinned repos
|
||||
pinnedRepos, err := org_service.GetOrgPinnedReposWithDetails(ctx, org.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Pinned groups
|
||||
pinnedGroups, err := organization.GetOrgPinnedGroups(ctx, org.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Public members
|
||||
publicMembers, totalMembers, err := organization.GetPublicOrgMembers(ctx, org.ID, 12)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Stats
|
||||
stats, err := org_service.GetOrgOverviewStats(ctx, org.ID, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Profile readme
|
||||
readme, _ := org_service.GetOrgProfileReadme(ctx, org.ID)
|
||||
|
||||
// Recent activity
|
||||
recentActivity, _ := org_service.GetOrgRecentActivity(ctx, org.ID, ctx.Doer, 10)
|
||||
|
||||
// Convert pinned repos
|
||||
apiPinnedRepos := make([]*api.OrgPinnedRepo, 0, len(pinnedRepos))
|
||||
for _, p := range pinnedRepos {
|
||||
if p.Repo == nil {
|
||||
continue
|
||||
}
|
||||
apiPinnedRepos = append(apiPinnedRepos, convertOrgPinnedRepoV2(ctx, p))
|
||||
}
|
||||
|
||||
apiPinnedGroups := make([]*api.OrgPinnedGroup, 0, len(pinnedGroups))
|
||||
for _, g := range pinnedGroups {
|
||||
apiPinnedGroups = append(apiPinnedGroups, convertOrgPinnedGroupV2(g))
|
||||
}
|
||||
|
||||
apiPublicMembers := make([]*api.OrgPublicMember, 0, len(publicMembers))
|
||||
for _, m := range publicMembers {
|
||||
apiPublicMembers = append(apiPublicMembers, &api.OrgPublicMember{
|
||||
User: convert.ToUser(ctx, m.User, ctx.Doer),
|
||||
Role: m.Role,
|
||||
})
|
||||
}
|
||||
|
||||
// Build profile content
|
||||
var profile *api.OrgProfileContent
|
||||
if readme != "" {
|
||||
profile = &api.OrgProfileContent{
|
||||
HasProfile: true,
|
||||
Readme: readme,
|
||||
}
|
||||
}
|
||||
|
||||
// Build recent activity
|
||||
var apiRecentActivity []api.OrgRecentActivity
|
||||
if len(recentActivity) > 0 {
|
||||
apiRecentActivity = make([]api.OrgRecentActivity, 0, len(recentActivity))
|
||||
for _, a := range recentActivity {
|
||||
apiRecentActivity = append(apiRecentActivity, api.OrgRecentActivity{
|
||||
RepoName: a.RepoName,
|
||||
RepoFullName: a.RepoFullName,
|
||||
DefaultBranch: a.DefaultBranch,
|
||||
CommitMessage: a.CommitMessage,
|
||||
CommitTime: a.CommitTime,
|
||||
IsPrivate: a.IsPrivate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
overview := &api.OrgOverviewV2{
|
||||
Organization: convert.ToOrganization(ctx, org),
|
||||
PinnedRepos: apiPinnedRepos,
|
||||
PinnedGroups: apiPinnedGroups,
|
||||
PublicMembers: apiPublicMembers,
|
||||
TotalMembers: totalMembers,
|
||||
Stats: &api.OrgOverviewStats{
|
||||
TotalRepos: stats.TotalRepos,
|
||||
TotalMembers: stats.TotalMembers,
|
||||
TotalTeams: stats.TotalTeams,
|
||||
TotalStars: stats.TotalStars,
|
||||
},
|
||||
Profile: profile,
|
||||
RecentActivity: apiRecentActivity,
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, overview)
|
||||
}
|
||||
|
||||
// --- Update Org ---
|
||||
|
||||
// UpdateOrgV2Option represents the fields that can be updated on an org.
|
||||
type UpdateOrgV2Option struct {
|
||||
FullName *string `json:"full_name"`
|
||||
Email *string `json:"email"`
|
||||
Description *string `json:"description"`
|
||||
Website *string `json:"website"`
|
||||
Location *string `json:"location"`
|
||||
GroupHeader *string `json:"group_header"`
|
||||
}
|
||||
|
||||
// UpdateOrgV2 updates an organization's basic information.
|
||||
func UpdateOrgV2(ctx *context.APIContext) {
|
||||
org := loadOrg(ctx)
|
||||
if org == nil {
|
||||
return
|
||||
}
|
||||
if !requireOrgOwner(ctx, org) {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*UpdateOrgV2Option)
|
||||
|
||||
opts := &user_service.UpdateOptions{}
|
||||
if form.FullName != nil {
|
||||
opts.FullName = optional.Some(*form.FullName)
|
||||
}
|
||||
if form.Description != nil {
|
||||
opts.Description = optional.Some(*form.Description)
|
||||
}
|
||||
if form.Website != nil {
|
||||
opts.Website = optional.Some(*form.Website)
|
||||
}
|
||||
if form.Location != nil {
|
||||
opts.Location = optional.Some(*form.Location)
|
||||
}
|
||||
if form.GroupHeader != nil {
|
||||
opts.GroupHeader = optional.Some(*form.GroupHeader)
|
||||
}
|
||||
|
||||
if form.Email != nil && *form.Email != "" {
|
||||
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), *form.Email); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Reload to get updated fields
|
||||
org, err := organization.GetOrgByName(ctx, org.Name)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, org))
|
||||
}
|
||||
|
||||
// --- List Org Repos ---
|
||||
|
||||
// ListOrgReposV2 lists all repos for an org, with optional group_header grouping.
|
||||
func ListOrgReposV2(ctx *context.APIContext) {
|
||||
org := loadOrg(ctx)
|
||||
if org == nil {
|
||||
return
|
||||
}
|
||||
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
limit := ctx.FormInt("limit")
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
groupBy := ctx.FormString("group_by")
|
||||
keyword := ctx.FormTrim("q")
|
||||
sortOrder := ctx.FormString("sort")
|
||||
|
||||
var orderBy db.SearchOrderBy
|
||||
switch sortOrder {
|
||||
case "newest":
|
||||
orderBy = db.SearchOrderByNewest
|
||||
case "oldest":
|
||||
orderBy = db.SearchOrderByOldest
|
||||
case "reversealphabetically":
|
||||
orderBy = db.SearchOrderByAlphabeticallyReverse
|
||||
case "stars":
|
||||
orderBy = db.SearchOrderByStarsReverse
|
||||
case "forks":
|
||||
orderBy = db.SearchOrderByForksReverse
|
||||
case "recentupdate":
|
||||
orderBy = db.SearchOrderByRecentUpdated
|
||||
default:
|
||||
orderBy = db.SearchOrderByAlphabetically
|
||||
}
|
||||
|
||||
// When grouping by group_header, prepend it to the sort
|
||||
if groupBy == "group_header" {
|
||||
orderBy = db.SearchOrderBy("CASE WHEN group_header = '' OR group_header IS NULL THEN 1 ELSE 0 END, group_header ASC, " + string(orderBy))
|
||||
}
|
||||
|
||||
repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{PageSize: limit, Page: page},
|
||||
Keyword: keyword,
|
||||
OwnerID: org.ID,
|
||||
OrderBy: orderBy,
|
||||
Private: ctx.IsSigned,
|
||||
Actor: ctx.Doer,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiRepos := make([]*api.Repository, 0, len(repos))
|
||||
for _, repo := range repos {
|
||||
apiRepos = append(apiRepos, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeRead}))
|
||||
}
|
||||
|
||||
if groupBy == "group_header" {
|
||||
// Return grouped response
|
||||
type groupedEntry struct {
|
||||
GroupHeader string `json:"group_header"`
|
||||
Repos []*api.Repository `json:"repos"`
|
||||
}
|
||||
grouped := make(map[string][]*api.Repository)
|
||||
var headers []string
|
||||
headerSeen := make(map[string]bool)
|
||||
|
||||
for i, repo := range repos {
|
||||
header := repo.GroupHeader
|
||||
if !headerSeen[header] {
|
||||
headerSeen[header] = true
|
||||
headers = append(headers, header)
|
||||
}
|
||||
grouped[header] = append(grouped[header], apiRepos[i])
|
||||
}
|
||||
|
||||
groups := make([]groupedEntry, 0, len(headers))
|
||||
for _, h := range headers {
|
||||
groups = append(groups, groupedEntry{
|
||||
GroupHeader: h,
|
||||
Repos: grouped[h],
|
||||
})
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"groups": groups,
|
||||
"total": count,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, apiRepos)
|
||||
}
|
||||
|
||||
// --- Profile Readme ---
|
||||
|
||||
// GetOrgProfileReadmeV2 returns the raw README content from the .profile repo.
|
||||
func GetOrgProfileReadmeV2(ctx *context.APIContext) {
|
||||
org := loadOrg(ctx)
|
||||
if org == nil {
|
||||
return
|
||||
}
|
||||
|
||||
readme, err := org_service.GetOrgProfileReadme(ctx, org.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"content": readme,
|
||||
"has_profile": readme != "",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateOrgProfileReadmeOption represents a request to update the profile readme.
|
||||
type UpdateOrgProfileReadmeOption struct {
|
||||
Content string `json:"content" binding:"Required"`
|
||||
CommitMessage string `json:"commit_message"`
|
||||
}
|
||||
|
||||
// UpdateOrgProfileReadmeV2 updates the README.md in the .profile repo.
|
||||
func UpdateOrgProfileReadmeV2(ctx *context.APIContext) {
|
||||
org := loadOrg(ctx)
|
||||
if org == nil {
|
||||
return
|
||||
}
|
||||
if !requireOrgOwner(ctx, org) {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*UpdateOrgProfileReadmeOption)
|
||||
|
||||
if err := org_service.UpdateOrgProfileReadme(ctx, ctx.Doer, org.ID, form.Content, form.CommitMessage); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"status": "ok",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Pinned Repos ---
|
||||
|
||||
// PinOrgRepoV2Option represents a request to pin a repo.
|
||||
type PinOrgRepoV2Option struct {
|
||||
RepoName string `json:"repo_name" binding:"Required"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
DisplayOrder int `json:"display_order"`
|
||||
}
|
||||
|
||||
// PinOrgRepoV2 pins a repository to the org overview.
|
||||
func PinOrgRepoV2(ctx *context.APIContext) {
|
||||
org := loadOrg(ctx)
|
||||
if org == nil {
|
||||
return
|
||||
}
|
||||
if !requireOrgOwner(ctx, org) {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*PinOrgRepoV2Option)
|
||||
|
||||
// Look up the repo
|
||||
repo, err := repo_model.GetRepositoryByName(ctx, org.ID, form.RepoName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
ctx.APIError(http.StatusNotFound, "Repository not found: "+form.RepoName)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already pinned
|
||||
isPinned, err := organization.IsRepoPinned(ctx, org.ID, repo.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if isPinned {
|
||||
ctx.JSON(http.StatusOK, map[string]any{"status": "already_pinned"})
|
||||
return
|
||||
}
|
||||
|
||||
pinned := &organization.OrgPinnedRepo{
|
||||
OrgID: org.ID,
|
||||
RepoID: repo.ID,
|
||||
GroupID: form.GroupID,
|
||||
DisplayOrder: form.DisplayOrder,
|
||||
}
|
||||
|
||||
if err := organization.CreateOrgPinnedRepo(ctx, pinned); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, map[string]any{
|
||||
"id": pinned.ID,
|
||||
"repo_id": repo.ID,
|
||||
"status": "pinned",
|
||||
})
|
||||
}
|
||||
|
||||
// UnpinOrgRepoV2 unpins a repository from the org overview.
|
||||
func UnpinOrgRepoV2(ctx *context.APIContext) {
|
||||
org := loadOrg(ctx)
|
||||
if org == nil {
|
||||
return
|
||||
}
|
||||
if !requireOrgOwner(ctx, org) {
|
||||
return
|
||||
}
|
||||
|
||||
repoName := ctx.PathParam("repo")
|
||||
repo, err := repo_model.GetRepositoryByName(ctx, org.ID, repoName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
ctx.APIError(http.StatusNotFound, "Repository not found: "+repoName)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := organization.DeleteOrgPinnedRepo(ctx, org.ID, repo.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// --- Converters ---
|
||||
|
||||
func convertOrgPinnedRepoV2(ctx *context.APIContext, p *organization.OrgPinnedRepo) *api.OrgPinnedRepo {
|
||||
result := &api.OrgPinnedRepo{
|
||||
ID: p.ID,
|
||||
RepoID: p.RepoID,
|
||||
GroupID: p.GroupID,
|
||||
DisplayOrder: p.DisplayOrder,
|
||||
}
|
||||
|
||||
if p.Repo != nil {
|
||||
if repo, ok := p.Repo.(*repo_model.Repository); ok {
|
||||
result.Repo = convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeRead})
|
||||
}
|
||||
}
|
||||
|
||||
if p.Group != nil {
|
||||
result.Group = convertOrgPinnedGroupV2(p.Group)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func convertOrgPinnedGroupV2(g *organization.OrgPinnedGroup) *api.OrgPinnedGroup {
|
||||
return &api.OrgPinnedGroup{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
DisplayOrder: g.DisplayOrder,
|
||||
Collapsed: g.Collapsed,
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,7 @@ func CheckAppUpdate(ctx *context.APIContext) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
latestRelease.Repo = repo
|
||||
|
||||
// Find the appropriate asset for this platform/arch
|
||||
downloadURL, platformInfo := findUpdateAsset(latestRelease, platform, arch)
|
||||
@@ -346,6 +347,7 @@ func ListReleasesV2(ctx *context.APIContext) {
|
||||
// Convert to API format
|
||||
apiReleases := make([]*api.Release, 0, len(releases))
|
||||
for _, release := range releases {
|
||||
release.Repo = repo
|
||||
apiReleases = append(apiReleases, convertToAPIRelease(repo, release))
|
||||
}
|
||||
|
||||
@@ -388,6 +390,7 @@ func GetReleaseV2(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
release.Repo = repo
|
||||
ctx.JSON(http.StatusOK, convertToAPIRelease(repo, release))
|
||||
}
|
||||
|
||||
@@ -436,6 +439,7 @@ func GetLatestReleaseV2(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
release.Repo = repo
|
||||
ctx.JSON(http.StatusOK, convertToAPIRelease(repo, release))
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,12 @@ func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, t
|
||||
}
|
||||
|
||||
opts.Keyword = ctx.FormTrim("q")
|
||||
opts.OrderBy = orderBy
|
||||
// When grouping is enabled, order by group first so groups aren't split across pages
|
||||
if ctx.Data["PageIsExploreOrganizations"] == true && ctx.Data["ShowGrouping"] == true {
|
||||
opts.OrderBy = "CASE WHEN `user`.group_header = '' OR `user`.group_header IS NULL THEN 1 ELSE 0 END, `user`.group_header ASC, " + orderBy
|
||||
} else {
|
||||
opts.OrderBy = orderBy
|
||||
}
|
||||
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
|
||||
users, count, err = user_model.SearchUsers(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -274,6 +274,85 @@ func renderLandingPage(ctx *context.Context, repo *repo_model.Repository, config
|
||||
}
|
||||
}
|
||||
|
||||
// Load cross-promoted repos if enabled, filtered to those with landing pages
|
||||
if config.CrossPromote.Enabled {
|
||||
records, err := repo_model.GetCrossPromotedRepos(ctx, repo.ID)
|
||||
if err != nil {
|
||||
log.Warn("GetCrossPromotedRepos for repo %d: %v", repo.ID, err)
|
||||
} else if len(records) > 0 {
|
||||
repoIDs := make([]int64, len(records))
|
||||
for i, r := range records {
|
||||
repoIDs[i] = r.TargetRepoID
|
||||
}
|
||||
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
|
||||
if err != nil {
|
||||
log.Warn("GetRepositoriesMapByIDs for cross-promote: %v", err)
|
||||
} else {
|
||||
type CrossPromoteItem struct {
|
||||
Name string
|
||||
Description string
|
||||
URL string
|
||||
LogoURL string
|
||||
}
|
||||
var items []CrossPromoteItem
|
||||
for _, r := range records {
|
||||
target, ok := repos[r.TargetRepoID]
|
||||
if !ok {
|
||||
log.Trace("Cross-promote target repo %d not found", r.TargetRepoID)
|
||||
continue
|
||||
}
|
||||
// Only require that the target repo has landing pages enabled —
|
||||
// the landing page is publicly reachable independent of the repo's
|
||||
// private flag, so we don't filter by target.IsPrivate.
|
||||
pagesEnabled, err := repo_model.IsPagesEnabled(ctx, target.ID)
|
||||
if err != nil || !pagesEnabled {
|
||||
log.Trace("Cross-promote target %s does not have pages enabled (err=%v, enabled=%v)", target.FullName(), err, pagesEnabled)
|
||||
continue
|
||||
}
|
||||
item := CrossPromoteItem{
|
||||
Description: target.Description,
|
||||
Name: target.Name,
|
||||
}
|
||||
// URL priority:
|
||||
// 1. Repo's explicit Website field (dev-controlled)
|
||||
// 2. Verified custom domain for pages
|
||||
// 3. Internal /pages URL
|
||||
switch {
|
||||
case target.Website != "":
|
||||
item.URL = target.Website
|
||||
default:
|
||||
item.URL = target.Link() + "/pages"
|
||||
if domains, err := repo_model.GetPagesDomainsByRepoID(ctx, target.ID); err == nil {
|
||||
for _, d := range domains {
|
||||
if d.Verified {
|
||||
item.URL = "https://" + d.Domain
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try to load brand info from pages config (optional)
|
||||
if targetConfig, err := pages_service.GetPagesConfig(ctx, target); err == nil && targetConfig != nil {
|
||||
if targetConfig.Brand.Name != "" {
|
||||
item.Name = targetConfig.Brand.Name
|
||||
}
|
||||
if targetConfig.Brand.LogoURL != "" {
|
||||
item.LogoURL = targetConfig.Brand.LogoURL
|
||||
} else if targetConfig.Brand.UploadedLogo != "" {
|
||||
item.LogoURL = "/repo-avatars/" + targetConfig.Brand.UploadedLogo
|
||||
}
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if len(items) > 0 {
|
||||
ctx.Data["CrossPromoteItems"] = items
|
||||
} else {
|
||||
log.Trace("Cross-promote enabled for repo %d but no eligible target repos", repo.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tpl := selectTemplate(config.Template)
|
||||
ctx.HTML(http.StatusOK, tpl)
|
||||
}
|
||||
@@ -1337,6 +1416,9 @@ func ensureTemplateDefaults(config *pages_module.LandingConfig) {
|
||||
if nav.LabelCompare == "" {
|
||||
nav.LabelCompare = defaults.LabelCompare
|
||||
}
|
||||
if nav.LabelCrossPromote == "" {
|
||||
nav.LabelCrossPromote = defaults.LabelCrossPromote
|
||||
}
|
||||
// Section headlines — fill empty headlines with sensible defaults
|
||||
// so they appear in the base JSON and can be overridden by translations.
|
||||
if config.Blog.Enabled && config.Blog.Headline == "" {
|
||||
@@ -1348,6 +1430,9 @@ func ensureTemplateDefaults(config *pages_module.LandingConfig) {
|
||||
if config.Comparison.Enabled && config.Comparison.Headline == "" {
|
||||
config.Comparison.Headline = "How We Compare"
|
||||
}
|
||||
if config.CrossPromote.Enabled && config.CrossPromote.Headline == "" {
|
||||
config.CrossPromote.Headline = "Related Offerings"
|
||||
}
|
||||
}
|
||||
|
||||
// ApproveExperiment handles the email approval link for an A/B test experiment
|
||||
|
||||
@@ -111,6 +111,9 @@ func applyTemplateDefaultLabels(config *pages_module.LandingConfig) {
|
||||
if nav.LabelCompare == "" {
|
||||
nav.LabelCompare = defaults.LabelCompare
|
||||
}
|
||||
if nav.LabelCrossPromote == "" {
|
||||
nav.LabelCrossPromote = defaults.LabelCrossPromote
|
||||
}
|
||||
}
|
||||
|
||||
// setCommonPagesData sets common data for all pages settings pages
|
||||
@@ -479,6 +482,12 @@ func PagesContentPost(ctx *context.Context) {
|
||||
config.Advanced.HideMobileReleases = ctx.FormBool("hide_mobile_releases")
|
||||
config.Advanced.GooglePlayID = strings.TrimSpace(ctx.FormString("google_play_id"))
|
||||
config.Advanced.AppStoreID = strings.TrimSpace(ctx.FormString("app_store_id"))
|
||||
if v := ctx.FormString("label_value_props"); v != "" {
|
||||
config.Navigation.LabelValueProps = v
|
||||
}
|
||||
if v := ctx.FormString("label_features"); v != "" {
|
||||
config.Navigation.LabelFeatures = v
|
||||
}
|
||||
config.Navigation.ShowDocs = ctx.FormBool("nav_show_docs")
|
||||
config.Navigation.ShowAPI = ctx.FormBool("nav_show_api")
|
||||
config.Navigation.ShowRepository = ctx.FormBool("nav_show_repository")
|
||||
@@ -508,6 +517,8 @@ func PagesContentPost(ctx *context.Context) {
|
||||
}
|
||||
config.Stats = append(config.Stats, pages_module.StatConfig{Value: value, Label: label})
|
||||
}
|
||||
config.ValuePropsHeadline = ctx.FormString("value_props_headline")
|
||||
config.ValuePropsSubheadline = ctx.FormString("value_props_subheadline")
|
||||
config.ValueProps = nil
|
||||
for i := range 10 {
|
||||
title := ctx.FormString(fmt.Sprintf("valueprop_title_%d", i))
|
||||
@@ -518,6 +529,8 @@ func PagesContentPost(ctx *context.Context) {
|
||||
}
|
||||
config.ValueProps = append(config.ValueProps, pages_module.ValuePropConfig{Title: title, Description: desc, Icon: icon})
|
||||
}
|
||||
config.FeaturesHeadline = ctx.FormString("features_headline")
|
||||
config.FeaturesSubheadline = ctx.FormString("features_subheadline")
|
||||
config.Features = nil
|
||||
for i := range 20 {
|
||||
title := ctx.FormString(fmt.Sprintf("feature_title_%d", i))
|
||||
@@ -531,6 +544,12 @@ func PagesContentPost(ctx *context.Context) {
|
||||
}
|
||||
// Comparison enabled toggle (full config is on /settings/pages/comparison)
|
||||
config.Comparison.Enabled = ctx.FormBool("comparison_enabled")
|
||||
// Cross-promote section
|
||||
config.CrossPromote.Enabled = ctx.FormBool("cross_promote_enabled")
|
||||
if v := ctx.FormString("cross_promote_headline"); v != "" {
|
||||
config.CrossPromote.Headline = v
|
||||
}
|
||||
config.CrossPromote.Subheadline = ctx.FormString("cross_promote_subheadline")
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("SavePagesConfig", err)
|
||||
return
|
||||
@@ -778,6 +797,12 @@ type TranslationView struct {
|
||||
CTAHeadline string
|
||||
CTASubheadline string
|
||||
CTAButton string
|
||||
// Value Props section
|
||||
ValuePropsHeadline string
|
||||
ValuePropsSubheadline string
|
||||
// Features section
|
||||
FeaturesHeadline string
|
||||
FeaturesSubheadline string
|
||||
// Blog
|
||||
BlogHeadline string
|
||||
BlogSubheadline string
|
||||
@@ -788,6 +813,9 @@ type TranslationView struct {
|
||||
// Comparison
|
||||
ComparisonHeadline string
|
||||
ComparisonSubheadline string
|
||||
// Cross-Promote
|
||||
CrossPromoteHeadline string
|
||||
CrossPromoteSubheadline string
|
||||
// Footer
|
||||
FooterCopyright string
|
||||
FooterLinkLabels []string
|
||||
@@ -795,16 +823,17 @@ type TranslationView struct {
|
||||
SEOTitle string
|
||||
SEODescription string
|
||||
// Navigation labels
|
||||
NavLabelValueProps string
|
||||
NavLabelFeatures string
|
||||
NavLabelPricing string
|
||||
NavLabelBlog string
|
||||
NavLabelGallery string
|
||||
NavLabelCompare string
|
||||
NavLabelDocs string
|
||||
NavLabelReleases string
|
||||
NavLabelAPI string
|
||||
NavLabelIssues string
|
||||
NavLabelValueProps string
|
||||
NavLabelFeatures string
|
||||
NavLabelPricing string
|
||||
NavLabelBlog string
|
||||
NavLabelGallery string
|
||||
NavLabelCompare string
|
||||
NavLabelCrossPromote string
|
||||
NavLabelDocs string
|
||||
NavLabelReleases string
|
||||
NavLabelAPI string
|
||||
NavLabelIssues string
|
||||
}
|
||||
|
||||
// overlayString extracts a string from a map
|
||||
@@ -874,10 +903,14 @@ func parseTranslationView(t *pages_model.Translation, config *pages_module.Landi
|
||||
// Value Props
|
||||
view.ValuePropTitles = overlayStringSlice(overlay, "value_props", "title", len(config.ValueProps))
|
||||
view.ValuePropDescs = overlayStringSlice(overlay, "value_props", "description", len(config.ValueProps))
|
||||
view.ValuePropsHeadline = overlayString(overlay, "value_props_headline")
|
||||
view.ValuePropsSubheadline = overlayString(overlay, "value_props_subheadline")
|
||||
|
||||
// Features
|
||||
view.FeatureTitles = overlayStringSlice(overlay, "features", "title", len(config.Features))
|
||||
view.FeatureDescs = overlayStringSlice(overlay, "features", "description", len(config.Features))
|
||||
view.FeaturesHeadline = overlayString(overlay, "features_headline")
|
||||
view.FeaturesSubheadline = overlayString(overlay, "features_subheadline")
|
||||
|
||||
// Testimonials (stored under social_proof.testimonials)
|
||||
view.TestimonialQuotes = make([]string, len(config.SocialProof.Testimonials))
|
||||
@@ -955,6 +988,12 @@ func parseTranslationView(t *pages_model.Translation, config *pages_module.Landi
|
||||
view.ComparisonSubheadline = overlayString(comp, "subheadline")
|
||||
}
|
||||
|
||||
// Cross-Promote
|
||||
if cp, ok := overlay["cross_promote"].(map[string]any); ok {
|
||||
view.CrossPromoteHeadline = overlayString(cp, "headline")
|
||||
view.CrossPromoteSubheadline = overlayString(cp, "subheadline")
|
||||
}
|
||||
|
||||
// Footer
|
||||
view.FooterLinkLabels = make([]string, len(config.Footer.Links))
|
||||
if footer, ok := overlay["footer"].(map[string]any); ok {
|
||||
@@ -985,6 +1024,7 @@ func parseTranslationView(t *pages_model.Translation, config *pages_module.Landi
|
||||
view.NavLabelBlog = overlayString(nav, "label_blog")
|
||||
view.NavLabelGallery = overlayString(nav, "label_gallery")
|
||||
view.NavLabelCompare = overlayString(nav, "label_compare")
|
||||
view.NavLabelCrossPromote = overlayString(nav, "label_cross_promote")
|
||||
view.NavLabelDocs = overlayString(nav, "label_docs")
|
||||
view.NavLabelReleases = overlayString(nav, "label_releases")
|
||||
view.NavLabelAPI = overlayString(nav, "label_api")
|
||||
@@ -1062,6 +1102,12 @@ func buildTranslationJSON(ctx *context.Context) string {
|
||||
if len(valueProps) > 0 {
|
||||
overlay["value_props"] = valueProps
|
||||
}
|
||||
if v := ctx.FormString("trans_value_props_headline"); v != "" {
|
||||
overlay["value_props_headline"] = v
|
||||
}
|
||||
if v := ctx.FormString("trans_value_props_subheadline"); v != "" {
|
||||
overlay["value_props_subheadline"] = v
|
||||
}
|
||||
|
||||
// Features (indexed)
|
||||
var features []map[string]any
|
||||
@@ -1079,6 +1125,12 @@ func buildTranslationJSON(ctx *context.Context) string {
|
||||
if len(features) > 0 {
|
||||
overlay["features"] = features
|
||||
}
|
||||
if v := ctx.FormString("trans_features_headline"); v != "" {
|
||||
overlay["features_headline"] = v
|
||||
}
|
||||
if v := ctx.FormString("trans_features_subheadline"); v != "" {
|
||||
overlay["features_subheadline"] = v
|
||||
}
|
||||
|
||||
// Testimonials (indexed)
|
||||
var testimonials []map[string]any
|
||||
@@ -1178,6 +1230,18 @@ func buildTranslationJSON(ctx *context.Context) string {
|
||||
overlay["comparison"] = comp
|
||||
}
|
||||
|
||||
// Cross-Promote
|
||||
cp := map[string]any{}
|
||||
if v := ctx.FormString("trans_cross_promote_headline"); v != "" {
|
||||
cp["headline"] = v
|
||||
}
|
||||
if v := ctx.FormString("trans_cross_promote_subheadline"); v != "" {
|
||||
cp["subheadline"] = v
|
||||
}
|
||||
if len(cp) > 0 {
|
||||
overlay["cross_promote"] = cp
|
||||
}
|
||||
|
||||
// Footer
|
||||
footer := map[string]any{}
|
||||
if v := ctx.FormString("trans_footer_copyright"); v != "" {
|
||||
@@ -1216,7 +1280,7 @@ func buildTranslationJSON(ctx *context.Context) string {
|
||||
nav := map[string]any{}
|
||||
for _, key := range []string{
|
||||
"label_value_props", "label_features", "label_pricing",
|
||||
"label_blog", "label_gallery", "label_compare",
|
||||
"label_blog", "label_gallery", "label_compare", "label_cross_promote",
|
||||
"label_docs", "label_releases", "label_api", "label_issues",
|
||||
} {
|
||||
if v := ctx.FormString("trans_nav_" + key); v != "" {
|
||||
@@ -1533,10 +1597,6 @@ func PagesAdvancedPost(ctx *context.Context) {
|
||||
// Parse remaining fields
|
||||
config.Advanced.CustomCSS = ctx.FormString("custom_css")
|
||||
config.Advanced.CustomHead = ctx.FormString("custom_head")
|
||||
config.Advanced.GooglePlayID = ctx.FormString("google_play_id")
|
||||
config.Advanced.AppStoreID = ctx.FormString("app_store_id")
|
||||
config.Advanced.PublicReleases = ctx.FormBool("public_releases")
|
||||
config.Advanced.HideMobileReleases = ctx.FormBool("hide_mobile_releases")
|
||||
|
||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||
ctx.ServerError("SavePagesConfig", err)
|
||||
|
||||
@@ -470,12 +470,13 @@ func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
|
||||
}
|
||||
|
||||
m := &repo_model.PushMirror{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
RemoteName: "remote_mirror_" + remoteSuffix,
|
||||
SyncOnCommit: form.PushMirrorSyncOnCommit,
|
||||
Interval: interval,
|
||||
RemoteAddress: remoteAddress,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
RemoteName: "remote_mirror_" + remoteSuffix,
|
||||
SyncOnCommit: form.PushMirrorSyncOnCommit,
|
||||
ExcludeHiddenFiles: form.PushMirrorExcludeHiddenFiles,
|
||||
Interval: interval,
|
||||
RemoteAddress: remoteAddress,
|
||||
}
|
||||
if err := db.Insert(ctx, m); err != nil {
|
||||
ctx.ServerError("InsertPushMirror", err)
|
||||
|
||||
@@ -89,26 +89,27 @@ func (f *MigrateRepoForm) Validate(req *http.Request, errs binding.Errors) bindi
|
||||
|
||||
// RepoSettingForm form for changing repository settings
|
||||
type RepoSettingForm struct {
|
||||
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
|
||||
Description string `binding:"MaxSize(2048)"`
|
||||
DisplayTitle string `binding:"MaxSize(255)"`
|
||||
GroupHeader string `binding:"MaxSize(255)"`
|
||||
OwnerDisplayName string `binding:"MaxSize(255)"`
|
||||
Website string `binding:"ValidUrl;MaxSize(1024)"`
|
||||
Interval string
|
||||
MirrorAddress string
|
||||
MirrorUsername string
|
||||
MirrorPassword string
|
||||
LFS bool `form:"mirror_lfs"`
|
||||
LFSEndpoint string `form:"mirror_lfs_endpoint"`
|
||||
PushMirrorID int64
|
||||
PushMirrorAddress string
|
||||
PushMirrorUsername string
|
||||
PushMirrorPassword string
|
||||
PushMirrorSyncOnCommit bool
|
||||
PushMirrorInterval string
|
||||
Template bool
|
||||
EnablePrune bool
|
||||
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
|
||||
Description string `binding:"MaxSize(2048)"`
|
||||
DisplayTitle string `binding:"MaxSize(255)"`
|
||||
GroupHeader string `binding:"MaxSize(255)"`
|
||||
OwnerDisplayName string `binding:"MaxSize(255)"`
|
||||
Website string `binding:"ValidUrl;MaxSize(1024)"`
|
||||
Interval string
|
||||
MirrorAddress string
|
||||
MirrorUsername string
|
||||
MirrorPassword string
|
||||
LFS bool `form:"mirror_lfs"`
|
||||
LFSEndpoint string `form:"mirror_lfs_endpoint"`
|
||||
PushMirrorID int64
|
||||
PushMirrorAddress string
|
||||
PushMirrorUsername string
|
||||
PushMirrorPassword string
|
||||
PushMirrorSyncOnCommit bool
|
||||
PushMirrorExcludeHiddenFiles bool
|
||||
PushMirrorInterval string
|
||||
Template bool
|
||||
EnablePrune bool
|
||||
|
||||
// Advanced settings
|
||||
EnableCode bool
|
||||
|
||||
@@ -28,6 +28,82 @@ import (
|
||||
|
||||
var stripExitStatus = regexp.MustCompile(`exit status \d+ - `)
|
||||
|
||||
// buildFilterOpts builds a FilterTreeOptions from the repo's hidden file settings.
|
||||
func buildFilterOpts(ctx context.Context, repo *repo_model.Repository) *git.FilterTreeOptions {
|
||||
opts := &git.FilterTreeOptions{
|
||||
ExcludeDotFiles: repo.HideDotfiles,
|
||||
}
|
||||
if hiddenPaths, err := repo_model.GetHiddenFolderPaths(ctx, repo.ID); err == nil && len(hiddenPaths) > 0 {
|
||||
opts.ExcludePaths = hiddenPaths
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// performFilteredPush pushes each branch and tag with dot folders/hidden paths stripped.
|
||||
// Uses git plumbing to build filtered commits (no refs created, no artifacts left behind).
|
||||
func performFilteredPush(ctx context.Context, repo *repo_model.Repository, remoteName string, filterOpts *git.FilterTreeOptions, timeout time.Duration) error {
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open repository: %w", err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
remoteURL, err := gitrepo.GitRemoteGetURL(ctx, repo, remoteName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get remote URL: %w", err)
|
||||
}
|
||||
envs := proxy.EnvWithProxy(remoteURL.URL)
|
||||
|
||||
// Push each branch with filtered content
|
||||
branches, _, err := gitRepo.GetBranchNames(0, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get branches: %w", err)
|
||||
}
|
||||
|
||||
for _, branch := range branches {
|
||||
commitID, err := gitRepo.GetBranchCommitID(branch)
|
||||
if err != nil {
|
||||
log.Warn("GetBranchCommitID(%s): %v", branch, err)
|
||||
continue
|
||||
}
|
||||
|
||||
filteredSHA, err := gitRepo.BuildFilteredCommit(commitID, filterOpts)
|
||||
if err != nil {
|
||||
log.Warn("BuildFilteredCommit(%s/%s): %v", branch, commitID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Trace("Push mirror filtered [repo: %-v] branch %s: %s -> %s", repo, branch, commitID[:12], filteredSHA[:12])
|
||||
|
||||
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
|
||||
Remote: remoteName,
|
||||
Force: true,
|
||||
Branch: "refs/heads/" + branch,
|
||||
LocalRefName: filteredSHA,
|
||||
Timeout: timeout,
|
||||
Env: envs,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("push branch %s: %w", branch, util.SanitizeErrorCredentialURLs(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Push tags as-is (tags point at commits, not trees, so dot folders in the
|
||||
// tagged tree are acceptable — the tag itself has no file content).
|
||||
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
|
||||
Remote: remoteName,
|
||||
Force: true,
|
||||
Branch: "refs/tags/*",
|
||||
LocalRefName: "refs/tags/*",
|
||||
Timeout: timeout,
|
||||
Env: envs,
|
||||
}); err != nil {
|
||||
// Non-fatal — tags may not exist or remote may reject
|
||||
log.Trace("Push mirror tags [repo: %-v]: %v", repo, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddPushMirrorRemote registers the push mirror remote.
|
||||
func AddPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) error {
|
||||
addRemoteAndConfig := func(storageRepo gitrepo.Repository, addr string) error {
|
||||
@@ -168,6 +244,43 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use filtered push if ExcludeHiddenFiles is enabled and the repo has hidden content
|
||||
if m.ExcludeHiddenFiles {
|
||||
filterOpts := buildFilterOpts(ctx, m.Repo)
|
||||
if filterOpts.HasFilters() {
|
||||
log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Using filtered push (exclude hidden files)", m.ID, m.Repo)
|
||||
|
||||
// LFS sync still happens normally
|
||||
if setting.LFS.StartServer {
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, m.Repo)
|
||||
if err == nil {
|
||||
remoteURL, err := gitrepo.GitRemoteGetURL(ctx, m.Repo, m.RemoteName)
|
||||
if err == nil {
|
||||
endpoint := lfs.DetermineEndpoint(remoteURL.String(), "")
|
||||
lfsClient := lfs.NewClient(endpoint, nil)
|
||||
if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil {
|
||||
gitRepo.Close()
|
||||
return util.SanitizeErrorCredentialURLs(err)
|
||||
}
|
||||
}
|
||||
gitRepo.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if err := performFilteredPush(ctx, m.Repo, m.RemoteName, filterOpts, timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wiki is always pushed unfiltered (no dot folders concern)
|
||||
if repo_service.HasWiki(ctx, m.Repo) {
|
||||
if _, err := gitrepo.GitRemoteGetURL(ctx, m.Repo.WikiStorageRepo(), m.RemoteName); err == nil {
|
||||
_ = performPush(m.Repo, true)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err := performPush(m.Repo, false)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
218
services/org/profile.go
Normal file
218
services/org/profile.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
user_model "code.gitcaddy.com/server/v3/models/user"
|
||||
"code.gitcaddy.com/server/v3/modules/git"
|
||||
repo_service "code.gitcaddy.com/server/v3/services/repository"
|
||||
files_service "code.gitcaddy.com/server/v3/services/repository/files"
|
||||
)
|
||||
|
||||
// GetOrgProfileReadme reads the README.md from the org's .profile repository.
|
||||
// Returns empty string if the .profile repo doesn't exist or has no README.
|
||||
func GetOrgProfileReadme(ctx context.Context, orgID int64) (string, error) {
|
||||
profileRepo, err := repo_model.GetRepositoryByName(ctx, orgID, ".profile")
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if profileRepo.IsEmpty {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, profileRepo.RepoPath())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetBranchCommit(profileRepo.DefaultBranch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Try common README filenames
|
||||
readmeFiles := []string{"README.md", "readme.md", "Readme.md", "README", "README.txt"}
|
||||
for _, filename := range readmeFiles {
|
||||
entry, err := commit.GetTreeEntryByPath(filename)
|
||||
if err == nil && !entry.IsDir() {
|
||||
content, err := entry.Blob().GetBlobContent(1024 * 512) // 512KB max
|
||||
if err == nil {
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// UpdateOrgProfileReadme updates (or creates) the README.md in the org's .profile repository.
|
||||
// If the .profile repo doesn't exist, it is created first.
|
||||
func UpdateOrgProfileReadme(ctx context.Context, doer *user_model.User, orgID int64, content, commitMessage string) error {
|
||||
// Look up the org as a user (needed for repo operations)
|
||||
orgUser, err := user_model.GetUserByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
profileRepo, err := repo_model.GetRepositoryByName(ctx, orgID, ".profile")
|
||||
if err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create .profile repo
|
||||
profileRepo, err = repo_service.CreateRepository(ctx, doer, orgUser, repo_service.CreateRepoOptions{
|
||||
Name: ".profile",
|
||||
Description: "Organization profile",
|
||||
AutoInit: true,
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
IsPrivate: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if commitMessage == "" {
|
||||
commitMessage = "Update organization profile README"
|
||||
}
|
||||
|
||||
// Determine operation: create or update
|
||||
operation := "update"
|
||||
var existingSHA string
|
||||
|
||||
if !profileRepo.IsEmpty {
|
||||
gitRepo, err := git.OpenRepository(ctx, profileRepo.RepoPath())
|
||||
if err == nil {
|
||||
commit, err := gitRepo.GetBranchCommit(profileRepo.DefaultBranch)
|
||||
if err == nil {
|
||||
entry, err := commit.GetTreeEntryByPath("README.md")
|
||||
if err != nil {
|
||||
operation = "create"
|
||||
} else {
|
||||
existingSHA = entry.ID.String()
|
||||
}
|
||||
}
|
||||
gitRepo.Close()
|
||||
}
|
||||
} else {
|
||||
operation = "create"
|
||||
}
|
||||
|
||||
_, err = files_service.ChangeRepoFiles(ctx, profileRepo, doer, &files_service.ChangeRepoFilesOptions{
|
||||
OldBranch: profileRepo.DefaultBranch,
|
||||
NewBranch: profileRepo.DefaultBranch,
|
||||
Message: commitMessage,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: operation,
|
||||
TreePath: "README.md",
|
||||
ContentReader: strings.NewReader(content),
|
||||
SHA: existingSHA,
|
||||
},
|
||||
},
|
||||
Author: &files_service.IdentityOptions{
|
||||
GitUserName: doer.Name,
|
||||
GitUserEmail: doer.Email,
|
||||
},
|
||||
})
|
||||
|
||||
// If update failed because file doesn't exist, try create
|
||||
if err != nil && operation == "update" {
|
||||
_, err = files_service.ChangeRepoFiles(ctx, profileRepo, doer, &files_service.ChangeRepoFilesOptions{
|
||||
OldBranch: profileRepo.DefaultBranch,
|
||||
NewBranch: profileRepo.DefaultBranch,
|
||||
Message: commitMessage,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "README.md",
|
||||
ContentReader: strings.NewReader(content),
|
||||
},
|
||||
},
|
||||
Author: &files_service.IdentityOptions{
|
||||
GitUserName: doer.Name,
|
||||
GitUserEmail: doer.Email,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOrgProfileRepo returns the .profile repository for an org, or nil if it doesn't exist.
|
||||
func GetOrgProfileRepo(ctx context.Context, orgID int64) (*repo_model.Repository, error) {
|
||||
repo, err := repo_model.GetRepositoryByName(ctx, orgID, ".profile")
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// GetOrgRecentActivity returns the N most recently updated repos for an org
|
||||
// with their latest commit info.
|
||||
type RecentRepoActivity struct {
|
||||
RepoName string `json:"repo_name"`
|
||||
RepoFullName string `json:"repo_full_name"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
CommitMessage string `json:"commit_message,omitempty"`
|
||||
CommitTime int64 `json:"commit_time,omitempty"`
|
||||
IsPrivate bool `json:"is_private"`
|
||||
}
|
||||
|
||||
func GetOrgRecentActivity(ctx context.Context, orgID int64, actor *user_model.User, limit int) ([]*RecentRepoActivity, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
showPrivate := actor != nil
|
||||
repos, _, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{PageSize: limit, Page: 1},
|
||||
OwnerID: orgID,
|
||||
OrderBy: db.SearchOrderByRecentUpdated,
|
||||
Private: showPrivate,
|
||||
Actor: actor,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*RecentRepoActivity, 0, len(repos))
|
||||
for _, repo := range repos {
|
||||
activity := &RecentRepoActivity{
|
||||
RepoName: repo.Name,
|
||||
RepoFullName: repo.FullName(),
|
||||
DefaultBranch: repo.DefaultBranch,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
||||
if err == nil {
|
||||
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
|
||||
if err == nil {
|
||||
activity.CommitMessage = commit.Summary()
|
||||
activity.CommitTime = commit.Author.When.Unix()
|
||||
}
|
||||
gitRepo.Close()
|
||||
}
|
||||
|
||||
result = append(result, activity)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -209,12 +209,55 @@ func TranslateLandingPageContent(ctx context.Context, repo *repo_model.Repositor
|
||||
|
||||
// Validate it's valid JSON
|
||||
result := extractJSON(resp.Result)
|
||||
var check map[string]any
|
||||
if err := json.Unmarshal([]byte(result), &check); err != nil {
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal([]byte(result), &parsed); err != nil {
|
||||
return "", fmt.Errorf("AI returned invalid JSON: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
// Strip placeholder values the AI sometimes returns for empty inputs
|
||||
// (e.g. "<UNKNOWN>", "<EMPTY>", "N/A") so they don't end up saved as translations.
|
||||
stripPlaceholders(parsed)
|
||||
|
||||
cleaned, err := json.Marshal(parsed)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("re-marshal translation: %w", err)
|
||||
}
|
||||
return string(cleaned), nil
|
||||
}
|
||||
|
||||
// stripPlaceholders walks a translation overlay and removes string values that
|
||||
// look like AI placeholders for empty input (e.g. "<UNKNOWN>", "<EMPTY>", "N/A").
|
||||
// This keeps stale "<UNKNOWN>" strings out of the saved translation overlay.
|
||||
func stripPlaceholders(v any) {
|
||||
switch t := v.(type) {
|
||||
case map[string]any:
|
||||
for k, child := range t {
|
||||
if s, ok := child.(string); ok && isPlaceholder(s) {
|
||||
delete(t, k)
|
||||
continue
|
||||
}
|
||||
stripPlaceholders(child)
|
||||
}
|
||||
case []any:
|
||||
for _, item := range t {
|
||||
stripPlaceholders(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isPlaceholder(s string) bool {
|
||||
switch strings.TrimSpace(strings.ToUpper(s)) {
|
||||
case "<UNKNOWN>", "<EMPTY>", "<NULL>", "N/A", "NONE":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// setIfNotEmpty assigns v to m[key] only if v is non-empty.
|
||||
func setIfNotEmpty(m map[string]any, key, v string) {
|
||||
if v != "" {
|
||||
m[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
// buildTranslatableContent extracts translatable text from a config for the AI
|
||||
@@ -223,18 +266,26 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
||||
|
||||
// Brand
|
||||
if config.Brand.Name != "" || config.Brand.Tagline != "" {
|
||||
content["brand"] = map[string]any{
|
||||
"name": config.Brand.Name,
|
||||
"tagline": config.Brand.Tagline,
|
||||
brand := map[string]any{}
|
||||
setIfNotEmpty(brand, "name", config.Brand.Name)
|
||||
setIfNotEmpty(brand, "tagline", config.Brand.Tagline)
|
||||
if len(brand) > 0 {
|
||||
content["brand"] = brand
|
||||
}
|
||||
}
|
||||
|
||||
// Hero
|
||||
content["hero"] = map[string]any{
|
||||
"headline": config.Hero.Headline,
|
||||
"subheadline": config.Hero.Subheadline,
|
||||
"primary_cta": map[string]string{"label": config.Hero.PrimaryCTA.Label},
|
||||
"secondary_cta": map[string]string{"label": config.Hero.SecondaryCTA.Label},
|
||||
// Hero — only include non-empty fields so the AI doesn't return "<UNKNOWN>"
|
||||
hero := map[string]any{}
|
||||
setIfNotEmpty(hero, "headline", config.Hero.Headline)
|
||||
setIfNotEmpty(hero, "subheadline", config.Hero.Subheadline)
|
||||
if config.Hero.PrimaryCTA.Label != "" {
|
||||
hero["primary_cta"] = map[string]string{"label": config.Hero.PrimaryCTA.Label}
|
||||
}
|
||||
if config.Hero.SecondaryCTA.Label != "" {
|
||||
hero["secondary_cta"] = map[string]string{"label": config.Hero.SecondaryCTA.Label}
|
||||
}
|
||||
if len(hero) > 0 {
|
||||
content["hero"] = hero
|
||||
}
|
||||
|
||||
// Stats, Value Props, Features (already struct-serializable)
|
||||
@@ -244,47 +295,78 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
||||
if len(config.ValueProps) > 0 {
|
||||
content["value_props"] = config.ValueProps
|
||||
}
|
||||
if config.ValuePropsHeadline != "" {
|
||||
content["value_props_headline"] = config.ValuePropsHeadline
|
||||
}
|
||||
if config.ValuePropsSubheadline != "" {
|
||||
content["value_props_subheadline"] = config.ValuePropsSubheadline
|
||||
}
|
||||
if len(config.Features) > 0 {
|
||||
content["features"] = config.Features
|
||||
}
|
||||
if config.FeaturesHeadline != "" {
|
||||
content["features_headline"] = config.FeaturesHeadline
|
||||
}
|
||||
if config.FeaturesSubheadline != "" {
|
||||
content["features_subheadline"] = config.FeaturesSubheadline
|
||||
}
|
||||
|
||||
// Testimonials
|
||||
if len(config.SocialProof.Testimonials) > 0 {
|
||||
testimonials := make([]map[string]string, 0, len(config.SocialProof.Testimonials))
|
||||
for _, t := range config.SocialProof.Testimonials {
|
||||
testimonials = append(testimonials, map[string]string{
|
||||
"quote": t.Quote,
|
||||
"role": t.Role,
|
||||
})
|
||||
tm := map[string]string{}
|
||||
if t.Quote != "" {
|
||||
tm["quote"] = t.Quote
|
||||
}
|
||||
if t.Role != "" {
|
||||
tm["role"] = t.Role
|
||||
}
|
||||
if len(tm) > 0 {
|
||||
testimonials = append(testimonials, tm)
|
||||
}
|
||||
}
|
||||
if len(testimonials) > 0 {
|
||||
content["social_proof"] = map[string]any{"testimonials": testimonials}
|
||||
}
|
||||
content["social_proof"] = map[string]any{"testimonials": testimonials}
|
||||
}
|
||||
|
||||
// Pricing
|
||||
if config.Pricing.Headline != "" || len(config.Pricing.Plans) > 0 {
|
||||
pricing := map[string]any{
|
||||
"headline": config.Pricing.Headline,
|
||||
"subheadline": config.Pricing.Subheadline,
|
||||
}
|
||||
pricing := map[string]any{}
|
||||
setIfNotEmpty(pricing, "headline", config.Pricing.Headline)
|
||||
setIfNotEmpty(pricing, "subheadline", config.Pricing.Subheadline)
|
||||
if len(config.Pricing.Plans) > 0 {
|
||||
plans := make([]map[string]string, 0, len(config.Pricing.Plans))
|
||||
for _, p := range config.Pricing.Plans {
|
||||
plans = append(plans, map[string]string{
|
||||
"name": p.Name,
|
||||
"period": p.Period,
|
||||
"cta": p.CTA,
|
||||
})
|
||||
plan := map[string]string{}
|
||||
if p.Name != "" {
|
||||
plan["name"] = p.Name
|
||||
}
|
||||
if p.Period != "" {
|
||||
plan["period"] = p.Period
|
||||
}
|
||||
if p.CTA != "" {
|
||||
plan["cta"] = p.CTA
|
||||
}
|
||||
plans = append(plans, plan)
|
||||
}
|
||||
pricing["plans"] = plans
|
||||
}
|
||||
content["pricing"] = pricing
|
||||
if len(pricing) > 0 {
|
||||
content["pricing"] = pricing
|
||||
}
|
||||
}
|
||||
|
||||
// CTA Section
|
||||
content["cta_section"] = map[string]any{
|
||||
"headline": config.CTASection.Headline,
|
||||
"subheadline": config.CTASection.Subheadline,
|
||||
"button": map[string]string{"label": config.CTASection.Button.Label},
|
||||
cta := map[string]any{}
|
||||
setIfNotEmpty(cta, "headline", config.CTASection.Headline)
|
||||
setIfNotEmpty(cta, "subheadline", config.CTASection.Subheadline)
|
||||
if config.CTASection.Button.Label != "" {
|
||||
cta["button"] = map[string]string{"label": config.CTASection.Button.Label}
|
||||
}
|
||||
if len(cta) > 0 {
|
||||
content["cta_section"] = cta
|
||||
}
|
||||
|
||||
// Blog
|
||||
@@ -294,9 +376,9 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
||||
blogHeadline = "Latest Posts"
|
||||
}
|
||||
blog := map[string]any{
|
||||
"headline": blogHeadline,
|
||||
"subheadline": config.Blog.Subheadline,
|
||||
"headline": blogHeadline,
|
||||
}
|
||||
setIfNotEmpty(blog, "subheadline", config.Blog.Subheadline)
|
||||
if config.Blog.CTAButton.Label != "" {
|
||||
blog["cta_button"] = map[string]string{"label": config.Blog.CTAButton.Label}
|
||||
}
|
||||
@@ -309,10 +391,11 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
||||
if galleryHeadline == "" {
|
||||
galleryHeadline = "Gallery"
|
||||
}
|
||||
content["gallery"] = map[string]any{
|
||||
"headline": galleryHeadline,
|
||||
"subheadline": config.Gallery.Subheadline,
|
||||
gallery := map[string]any{
|
||||
"headline": galleryHeadline,
|
||||
}
|
||||
setIfNotEmpty(gallery, "subheadline", config.Gallery.Subheadline)
|
||||
content["gallery"] = gallery
|
||||
}
|
||||
|
||||
// Comparison
|
||||
@@ -321,32 +404,53 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
||||
if compHeadline == "" {
|
||||
compHeadline = "How We Compare"
|
||||
}
|
||||
content["comparison"] = map[string]any{
|
||||
"headline": compHeadline,
|
||||
"subheadline": config.Comparison.Subheadline,
|
||||
comparison := map[string]any{
|
||||
"headline": compHeadline,
|
||||
}
|
||||
setIfNotEmpty(comparison, "subheadline", config.Comparison.Subheadline)
|
||||
content["comparison"] = comparison
|
||||
}
|
||||
|
||||
// Cross-Promote
|
||||
if config.CrossPromote.Enabled {
|
||||
cpHeadline := config.CrossPromote.Headline
|
||||
if cpHeadline == "" {
|
||||
cpHeadline = "Related Offerings"
|
||||
}
|
||||
crossPromote := map[string]any{
|
||||
"headline": cpHeadline,
|
||||
}
|
||||
setIfNotEmpty(crossPromote, "subheadline", config.CrossPromote.Subheadline)
|
||||
content["cross_promote"] = crossPromote
|
||||
}
|
||||
|
||||
// Footer
|
||||
if config.Footer.Copyright != "" || len(config.Footer.Links) > 0 {
|
||||
footer := map[string]any{
|
||||
"copyright": config.Footer.Copyright,
|
||||
}
|
||||
footer := map[string]any{}
|
||||
setIfNotEmpty(footer, "copyright", config.Footer.Copyright)
|
||||
if len(config.Footer.Links) > 0 {
|
||||
links := make([]map[string]string, 0, len(config.Footer.Links))
|
||||
for _, l := range config.Footer.Links {
|
||||
links = append(links, map[string]string{"label": l.Label})
|
||||
if l.Label != "" {
|
||||
links = append(links, map[string]string{"label": l.Label})
|
||||
}
|
||||
}
|
||||
if len(links) > 0 {
|
||||
footer["links"] = links
|
||||
}
|
||||
footer["links"] = links
|
||||
}
|
||||
content["footer"] = footer
|
||||
if len(footer) > 0 {
|
||||
content["footer"] = footer
|
||||
}
|
||||
}
|
||||
|
||||
// SEO
|
||||
if config.SEO.Title != "" || config.SEO.Description != "" {
|
||||
content["seo"] = map[string]any{
|
||||
"title": config.SEO.Title,
|
||||
"description": config.SEO.Description,
|
||||
seo := map[string]any{}
|
||||
setIfNotEmpty(seo, "title", config.SEO.Title)
|
||||
setIfNotEmpty(seo, "description", config.SEO.Description)
|
||||
if len(seo) > 0 {
|
||||
content["seo"] = seo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,16 +465,17 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
||||
return def
|
||||
}
|
||||
content["navigation"] = map[string]any{
|
||||
"label_value_props": labelOrDefault(config.Navigation.LabelValueProps, defaults.LabelValueProps),
|
||||
"label_features": labelOrDefault(config.Navigation.LabelFeatures, defaults.LabelFeatures),
|
||||
"label_pricing": labelOrDefault(config.Navigation.LabelPricing, defaults.LabelPricing),
|
||||
"label_blog": labelOrDefault(config.Navigation.LabelBlog, defaults.LabelBlog),
|
||||
"label_gallery": labelOrDefault(config.Navigation.LabelGallery, defaults.LabelGallery),
|
||||
"label_compare": labelOrDefault(config.Navigation.LabelCompare, defaults.LabelCompare),
|
||||
"label_docs": labelOrDefault(config.Navigation.LabelDocs, "Docs"),
|
||||
"label_releases": labelOrDefault(config.Navigation.LabelReleases, "Releases"),
|
||||
"label_api": labelOrDefault(config.Navigation.LabelAPI, "API"),
|
||||
"label_issues": labelOrDefault(config.Navigation.LabelIssues, "Issues"),
|
||||
"label_value_props": labelOrDefault(config.Navigation.LabelValueProps, defaults.LabelValueProps),
|
||||
"label_features": labelOrDefault(config.Navigation.LabelFeatures, defaults.LabelFeatures),
|
||||
"label_pricing": labelOrDefault(config.Navigation.LabelPricing, defaults.LabelPricing),
|
||||
"label_blog": labelOrDefault(config.Navigation.LabelBlog, defaults.LabelBlog),
|
||||
"label_gallery": labelOrDefault(config.Navigation.LabelGallery, defaults.LabelGallery),
|
||||
"label_compare": labelOrDefault(config.Navigation.LabelCompare, defaults.LabelCompare),
|
||||
"label_cross_promote": labelOrDefault(config.Navigation.LabelCrossPromote, defaults.LabelCrossPromote),
|
||||
"label_docs": labelOrDefault(config.Navigation.LabelDocs, "Docs"),
|
||||
"label_releases": labelOrDefault(config.Navigation.LabelReleases, "Releases"),
|
||||
"label_api": labelOrDefault(config.Navigation.LabelAPI, "API"),
|
||||
"label_issues": labelOrDefault(config.Navigation.LabelIssues, "Issues"),
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(content)
|
||||
|
||||
@@ -173,6 +173,20 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
||||
return nil, fmt.Errorf("createDelegateHooks: %w", err)
|
||||
}
|
||||
|
||||
// 5b - Filter hidden files from fork if source repo has HideDotfiles or hidden folders
|
||||
if opts.BaseRepo.HideDotfiles {
|
||||
filterOpts := &git.FilterTreeOptions{ExcludeDotFiles: true}
|
||||
if hiddenPaths, pathErr := repo_model.GetHiddenFolderPaths(ctx, opts.BaseRepo.ID); pathErr == nil && len(hiddenPaths) > 0 {
|
||||
filterOpts.ExcludePaths = hiddenPaths
|
||||
}
|
||||
if filterOpts.HasFilters() {
|
||||
if filterErr := filterForkBranches(ctx, repo, filterOpts); filterErr != nil {
|
||||
log.Error("filterForkBranches failed for %v: %v", repo, filterErr)
|
||||
// Non-fatal — fork still works, just has dot folders
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6 - Sync the repository branches and tags
|
||||
var gitRepo *git.Repository
|
||||
gitRepo, err = gitrepo.OpenRepository(ctx, repo)
|
||||
@@ -213,6 +227,47 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// filterForkBranches rewrites branch heads to exclude hidden/dot files.
|
||||
// For each branch, it builds a filtered commit (no refs left behind) and
|
||||
// updates the branch ref to point at the filtered commit.
|
||||
func filterForkBranches(ctx context.Context, repo *repo_model.Repository, filterOpts *git.FilterTreeOptions) error {
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open repository: %w", err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
branches, _, err := gitRepo.GetBranchNames(0, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get branches: %w", err)
|
||||
}
|
||||
|
||||
for _, branch := range branches {
|
||||
commitID, err := gitRepo.GetBranchCommitID(branch)
|
||||
if err != nil {
|
||||
log.Warn("filterForkBranches: GetBranchCommitID(%s): %v", branch, err)
|
||||
continue
|
||||
}
|
||||
|
||||
filteredSHA, err := gitRepo.BuildFilteredCommit(commitID, filterOpts)
|
||||
if err != nil {
|
||||
log.Warn("filterForkBranches: BuildFilteredCommit(%s): %v", branch, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if filteredSHA == commitID {
|
||||
continue // nothing filtered
|
||||
}
|
||||
|
||||
// Update the branch ref to point at the filtered commit
|
||||
if err := gitRepo.UpdateRef("refs/heads/"+branch, filteredSHA); err != nil {
|
||||
log.Warn("filterForkBranches: update-ref(%s): %v", branch, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConvertForkToNormalRepository convert the provided repo from a forked repo to normal repo
|
||||
func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Repository) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
|
||||
@@ -966,7 +966,19 @@
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.ad-nav { padding: 0 20px; }
|
||||
.ad-nav-links { display: none; }
|
||||
.ad-nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
padding: 16px 24px;
|
||||
gap: 8px;
|
||||
background: inherit;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
.ad-menu-toggle { display: flex; }
|
||||
.ad-hero { padding: 120px 24px 80px; }
|
||||
.ad-hero-ctas { flex-direction: column; align-items: center; }
|
||||
@@ -1016,6 +1028,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ad-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ad-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ad-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="ad-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="ad-nav-cta">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1276,7 +1289,8 @@
|
||||
<div class="ad-features-inner">
|
||||
<div class="ad-section-header ad-reveal">
|
||||
<div class="ad-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Systems Analysis{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="ad-features-grid">
|
||||
{{range .Config.ValueProps}}
|
||||
@@ -1299,7 +1313,8 @@
|
||||
<div class="ad-features-inner">
|
||||
<div class="ad-section-header ad-reveal">
|
||||
<div class="ad-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Technical Specifications{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</h2>
|
||||
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
|
||||
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="ad-features-grid">
|
||||
{{range .Config.Features}}
|
||||
@@ -1382,6 +1397,32 @@
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Cross-Promote Section -->
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
|
||||
<section class="ad-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
|
||||
<div class="ad-features-inner">
|
||||
<div class="ad-section-header ad-reveal">
|
||||
<div class="ad-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
|
||||
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
|
||||
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="ad-features-grid">
|
||||
{{range .CrossPromoteItems}}
|
||||
<a href="{{.URL}}" class="ad-feature-card ad-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
|
||||
{{if .LogoURL}}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
{{end}}
|
||||
<h3 class="ad-feature-title">{{.Name}}</h3>
|
||||
{{if .Description}}<p class="ad-feature-desc">{{.Description}}</p>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- CTA Section -->
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<section class="ad-cta-section" id="cta">
|
||||
|
||||
@@ -1065,7 +1065,19 @@
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nb-nav { padding: 14px 24px; }
|
||||
.nb-nav-links { display: none; }
|
||||
.nb-nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
padding: 16px 24px;
|
||||
gap: 8px;
|
||||
background: inherit;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
.nb-mobile-toggle { display: block; }
|
||||
.nb-mobile-menu { padding: 24px; }
|
||||
.nb-hero { padding: 120px 24px 80px; }
|
||||
@@ -1139,6 +1151,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="nb-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="nb-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="nb-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="nb-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="nb-nav-repo">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1179,6 +1192,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="nb-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="nb-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="nb-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="nb-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="nb-nav-repo">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1404,7 +1418,8 @@
|
||||
<section class="nb-value-props" id="value-props">
|
||||
<div class="nb-section-header nb-reveal">
|
||||
<div class="nb-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose this{{end}}</div>
|
||||
<h2>Unlock your <span class="nb-glow-primary">potential</span></h2>
|
||||
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}Unlock your <span class="nb-glow-primary">potential</span>{{end}}</h2>
|
||||
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="nb-value-grid">
|
||||
{{range .Config.ValueProps}}
|
||||
@@ -1425,7 +1440,8 @@
|
||||
<section class="nb-features" id="features">
|
||||
<div class="nb-section-header nb-reveal">
|
||||
<div class="nb-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
|
||||
<h2>Packed with <span class="nb-glow-text">power</span></h2>
|
||||
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Packed with <span class="nb-glow-text">power</span>{{end}}</h2>
|
||||
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="nb-feature-list">
|
||||
{{range .Config.Features}}
|
||||
@@ -1505,6 +1521,32 @@
|
||||
{{end}}
|
||||
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<!-- Cross-Promote Section -->
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
|
||||
<section class="nb-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
|
||||
<div class="nb-features-inner">
|
||||
<div class="nb-section-header nb-reveal">
|
||||
<div class="nb-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
|
||||
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
|
||||
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="nb-features-grid">
|
||||
{{range .CrossPromoteItems}}
|
||||
<a href="{{.URL}}" class="nb-feature-card nb-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
|
||||
{{if .LogoURL}}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
{{end}}
|
||||
<h3 class="nb-feature-title">{{.Name}}</h3>
|
||||
{{if .Description}}<p class="nb-feature-desc">{{.Description}}</p>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- CTA Banner -->
|
||||
<section class="nb-cta-section" id="cta">
|
||||
<div class="nb-cta-banner nb-reveal">
|
||||
|
||||
@@ -970,7 +970,19 @@
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.ct-nav { padding: 0 20px; }
|
||||
.ct-nav-links { display: none; }
|
||||
.ct-nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
padding: 16px 24px;
|
||||
gap: 8px;
|
||||
background: inherit;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
.ct-menu-toggle { display: flex; }
|
||||
.ct-hero { padding: 120px 20px 80px; }
|
||||
.ct-hero-ctas { flex-direction: column; align-items: center; }
|
||||
@@ -1022,6 +1034,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ct-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ct-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ct-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="ct-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="ct-nav-cta">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="14" height="14" alt="GitCaddy">
|
||||
@@ -1291,7 +1304,8 @@
|
||||
<div class="ct-features-inner">
|
||||
<div class="ct-section-header ct-reveal">
|
||||
<div class="ct-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose us{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="ct-features-grid">
|
||||
{{range .Config.ValueProps}}
|
||||
@@ -1314,7 +1328,8 @@
|
||||
<div class="ct-features-inner">
|
||||
<div class="ct-section-header ct-reveal">
|
||||
<div class="ct-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</h2>
|
||||
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
|
||||
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="ct-features-grid">
|
||||
{{range .Config.Features}}
|
||||
@@ -1397,6 +1412,32 @@
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Cross-Promote Section -->
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
|
||||
<section class="ct-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
|
||||
<div class="ct-features-inner">
|
||||
<div class="ct-section-header ct-reveal">
|
||||
<div class="ct-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
|
||||
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
|
||||
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="ct-features-grid">
|
||||
{{range .CrossPromoteItems}}
|
||||
<a href="{{.URL}}" class="ct-feature-card ct-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
|
||||
{{if .LogoURL}}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
{{end}}
|
||||
<h3 class="ct-feature-title">{{.Name}}</h3>
|
||||
{{if .Description}}<p class="ct-feature-desc">{{.Description}}</p>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- CTA Section -->
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<section class="ct-cta-section" id="cta">
|
||||
|
||||
@@ -855,7 +855,19 @@
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.dt-nav { padding: 0 20px; }
|
||||
.dt-nav-links { display: none; }
|
||||
.dt-nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
padding: 16px 24px;
|
||||
gap: 8px;
|
||||
background: inherit;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
.dt-menu-toggle { display: flex; }
|
||||
.dt-hero { padding: 100px 24px 80px; }
|
||||
.dt-hero-ctas { flex-direction: column; align-items: center; }
|
||||
@@ -905,6 +917,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="dt-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="dt-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="dt-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="dt-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="dt-nav-cta">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1164,7 +1177,8 @@
|
||||
<div class="dt-features-inner">
|
||||
<div class="dt-section-header dt-reveal">
|
||||
<div class="dt-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose us{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="dt-features-grid">
|
||||
{{range .Config.ValueProps}}
|
||||
@@ -1187,7 +1201,8 @@
|
||||
<div class="dt-features-inner">
|
||||
<div class="dt-section-header dt-reveal">
|
||||
<div class="dt-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</h2>
|
||||
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
|
||||
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="dt-features-grid">
|
||||
{{range .Config.Features}}
|
||||
@@ -1270,6 +1285,32 @@
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Cross-Promote Section -->
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
|
||||
<section class="dt-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
|
||||
<div class="dt-features-inner">
|
||||
<div class="dt-section-header dt-reveal">
|
||||
<div class="dt-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
|
||||
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
|
||||
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="dt-features-grid">
|
||||
{{range .CrossPromoteItems}}
|
||||
<a href="{{.URL}}" class="dt-feature-card dt-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
|
||||
{{if .LogoURL}}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
{{end}}
|
||||
<h3 class="dt-feature-title">{{.Name}}</h3>
|
||||
{{if .Description}}<p class="dt-feature-desc">{{.Description}}</p>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- CTA Section -->
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<section class="dt-cta-section" id="cta">
|
||||
|
||||
@@ -880,7 +880,19 @@
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.df-nav { padding: 0 20px; }
|
||||
.df-nav-links { display: none; }
|
||||
.df-nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
padding: 16px 24px;
|
||||
gap: 8px;
|
||||
background: inherit;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
.df-menu-toggle { display: flex; }
|
||||
.df-hero { padding: 120px 24px 80px; }
|
||||
.df-hero-ctas { flex-direction: column; align-items: center; }
|
||||
@@ -930,6 +942,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="df-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="df-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="df-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="df-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="df-nav-cta">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1188,7 +1201,8 @@
|
||||
<div class="df-features-inner">
|
||||
<div class="df-section-header df-reveal">
|
||||
<div class="df-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose us{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="df-features-grid">
|
||||
{{range .Config.ValueProps}}
|
||||
@@ -1211,7 +1225,8 @@
|
||||
<div class="df-features-inner">
|
||||
<div class="df-section-header df-reveal">
|
||||
<div class="df-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</h2>
|
||||
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
|
||||
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="df-features-grid">
|
||||
{{range .Config.Features}}
|
||||
@@ -1294,6 +1309,32 @@
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Cross-Promote Section -->
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
|
||||
<section class="df-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
|
||||
<div class="df-features-inner">
|
||||
<div class="df-section-header df-reveal">
|
||||
<div class="df-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
|
||||
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
|
||||
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="df-features-grid">
|
||||
{{range .CrossPromoteItems}}
|
||||
<a href="{{.URL}}" class="df-feature-card df-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
|
||||
{{if .LogoURL}}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
{{end}}
|
||||
<h3 class="df-feature-title">{{.Name}}</h3>
|
||||
{{if .Description}}<p class="df-feature-desc">{{.Description}}</p>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- CTA Section -->
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<section class="df-cta-section" id="cta">
|
||||
|
||||
@@ -1005,6 +1005,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ea-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ea-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ea-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="ea-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="ea-btn-text">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1040,6 +1041,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ea-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ea-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ea-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="ea-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="ea-btn-text">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1268,7 +1270,8 @@
|
||||
{{if .Config.ValueProps}}
|
||||
<section class="ea-section" id="why">
|
||||
<div class="ea-section-label ea-reveal">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose this{{end}}</div>
|
||||
<h2 class="ea-section-title ea-reveal">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
<h2 class="ea-section-title ea-reveal">{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
|
||||
{{range $i, $v := .Config.ValueProps}}
|
||||
<div class="ea-value-item ea-reveal">
|
||||
<div class="ea-value-number">{{$i | printf "%d"}}</div>
|
||||
@@ -1285,7 +1288,8 @@
|
||||
{{if .Config.Features}}
|
||||
<section class="ea-section" id="features">
|
||||
<div class="ea-section-label ea-reveal">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
|
||||
<h2 class="ea-section-title ea-reveal">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</h2>
|
||||
<h2 class="ea-section-title ea-reveal">{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
|
||||
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
|
||||
<div class="ea-accordion ea-reveal">
|
||||
{{range $i, $f := .Config.Features}}
|
||||
<div class="ea-accordion-item" data-index="{{$i}}">
|
||||
@@ -1372,6 +1376,32 @@
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Cross-Promote Section -->
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
|
||||
<section class="ea-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
|
||||
<div class="ea-features-inner">
|
||||
<div class="ea-section-header ea-reveal">
|
||||
<div class="ea-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
|
||||
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
|
||||
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="ea-features-grid">
|
||||
{{range .CrossPromoteItems}}
|
||||
<a href="{{.URL}}" class="ea-feature-card ea-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
|
||||
{{if .LogoURL}}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
{{end}}
|
||||
<h3 class="ea-feature-title">{{.Name}}</h3>
|
||||
{{if .Description}}<p class="ea-feature-desc">{{.Description}}</p>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- CTA Section -->
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<section class="ea-cta-section" id="cta">
|
||||
|
||||
@@ -926,7 +926,19 @@
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.osh-nav { padding: 0 20px; }
|
||||
.osh-nav-links { display: none; }
|
||||
.osh-nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
padding: 16px 24px;
|
||||
gap: 8px;
|
||||
background: inherit;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
.osh-menu-toggle { display: flex; }
|
||||
.osh-hero { padding: 120px 24px 80px; }
|
||||
.osh-hero-ctas { flex-direction: column; align-items: center; }
|
||||
@@ -996,6 +1008,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="osh-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="osh-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="osh-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="osh-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="osh-nav-cta">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1256,7 +1269,8 @@
|
||||
<div class="osh-features-inner">
|
||||
<div class="osh-section-header osh-reveal">
|
||||
<div class="osh-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose us{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="osh-features-grid">
|
||||
{{range .Config.ValueProps}}
|
||||
@@ -1279,7 +1293,8 @@
|
||||
<div class="osh-features-inner">
|
||||
<div class="osh-section-header osh-reveal">
|
||||
<div class="osh-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</h2>
|
||||
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
|
||||
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="osh-features-grid">
|
||||
{{range .Config.Features}}
|
||||
@@ -1362,6 +1377,32 @@
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Cross-Promote Section -->
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
|
||||
<section class="osh-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
|
||||
<div class="osh-features-inner">
|
||||
<div class="osh-section-header osh-reveal">
|
||||
<div class="osh-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
|
||||
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
|
||||
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="osh-features-grid">
|
||||
{{range .CrossPromoteItems}}
|
||||
<a href="{{.URL}}" class="osh-feature-card osh-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
|
||||
{{if .LogoURL}}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
{{end}}
|
||||
<h3 class="osh-feature-title">{{.Name}}</h3>
|
||||
{{if .Description}}<p class="osh-feature-desc">{{.Description}}</p>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- CTA Section -->
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<section class="osh-cta-section" id="cta">
|
||||
|
||||
@@ -1043,7 +1043,19 @@
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.gm-nav { top: 8px; left: 8px; right: 8px; padding: 12px 20px; }
|
||||
.gm-nav-links { display: none; }
|
||||
.gm-nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
padding: 16px 24px;
|
||||
gap: 8px;
|
||||
background: inherit;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
.gm-mobile-toggle { display: block; }
|
||||
.gm-hero { padding: 120px 24px 80px; }
|
||||
.gm-hero h1 { font-size: 36px; }
|
||||
@@ -1114,6 +1126,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="gm-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="gm-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="gm-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="gm-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="gm-nav-repo">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1154,6 +1167,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="gm-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="gm-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="gm-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="gm-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="gm-nav-repo">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1409,8 +1423,8 @@
|
||||
<section class="gm-section" id="value-props">
|
||||
<div class="gm-section-inner">
|
||||
<div class="gm-section-header gm-reveal">
|
||||
<h2>Why <span class="gm-serif">{{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}</span></h2>
|
||||
<p>Built for developers who value their time</p>
|
||||
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}Why <span class="gm-serif">{{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}</span>{{end}}</h2>
|
||||
<p>{{if .Config.ValuePropsSubheadline}}{{.Config.ValuePropsSubheadline}}{{else}}Built for developers who value their time{{end}}</p>
|
||||
</div>
|
||||
|
||||
<div class="gm-value-grid">
|
||||
@@ -1430,8 +1444,8 @@
|
||||
<section id="features" class="gm-section">
|
||||
<div class="gm-section-inner">
|
||||
<div class="gm-section-header gm-reveal">
|
||||
<h2>How it <span class="gm-serif">works</span></h2>
|
||||
<p>Get started in minutes, not weeks</p>
|
||||
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}How it <span class="gm-serif">works</span>{{end}}</h2>
|
||||
<p>{{if .Config.FeaturesSubheadline}}{{.Config.FeaturesSubheadline}}{{else}}Get started in minutes, not weeks{{end}}</p>
|
||||
</div>
|
||||
|
||||
<div class="gm-features-list">
|
||||
@@ -1651,6 +1665,32 @@
|
||||
|
||||
{{end}}{{/* end PageIsBlogDetail / PageIsBlogList / else */}}
|
||||
|
||||
<!-- Cross-Promote Section -->
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
|
||||
<section class="gm-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
|
||||
<div class="gm-features-inner">
|
||||
<div class="gm-section-header gm-reveal">
|
||||
<div class="gm-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
|
||||
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
|
||||
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="gm-features-grid">
|
||||
{{range .CrossPromoteItems}}
|
||||
<a href="{{.URL}}" class="gm-feature-card gm-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
|
||||
{{if .LogoURL}}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
{{end}}
|
||||
<h3 class="gm-feature-title">{{.Name}}</h3>
|
||||
{{if .Description}}<p class="gm-feature-desc">{{.Description}}</p>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="gm-footer">
|
||||
<div class="gm-footer-brand">
|
||||
|
||||
@@ -979,7 +979,19 @@
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.vs-nav { padding: 0 20px; }
|
||||
.vs-nav-links { display: none; }
|
||||
.vs-nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
padding: 16px 24px;
|
||||
gap: 8px;
|
||||
background: inherit;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
.vs-menu-toggle { display: flex; }
|
||||
.vs-hero { padding: 120px 24px 80px; }
|
||||
.vs-hero-ctas { flex-direction: column; align-items: center; }
|
||||
@@ -1032,6 +1044,7 @@
|
||||
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="vs-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
|
||||
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="vs-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
|
||||
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="vs-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="vs-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
|
||||
{{if .Config.Navigation.ShowRepository}}
|
||||
<a href="{{.RepoURL}}" class="vs-nav-cta">
|
||||
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
|
||||
@@ -1296,7 +1309,8 @@
|
||||
<div class="vs-features-inner">
|
||||
<div class="vs-section-header vs-reveal">
|
||||
<div class="vs-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose us{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
|
||||
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="vs-features-grid">
|
||||
{{range .Config.ValueProps}}
|
||||
@@ -1319,7 +1333,8 @@
|
||||
<div class="vs-features-inner">
|
||||
<div class="vs-section-header vs-reveal">
|
||||
<div class="vs-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
|
||||
<h2>{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</h2>
|
||||
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
|
||||
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="vs-features-grid">
|
||||
{{range .Config.Features}}
|
||||
@@ -1402,6 +1417,32 @@
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- Cross-Promote Section -->
|
||||
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
|
||||
<section class="vs-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
|
||||
<div class="vs-features-inner">
|
||||
<div class="vs-section-header vs-reveal">
|
||||
<div class="vs-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
|
||||
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
|
||||
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
|
||||
</div>
|
||||
<div class="vs-features-grid">
|
||||
{{range .CrossPromoteItems}}
|
||||
<a href="{{.URL}}" class="vs-feature-card vs-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
|
||||
{{if .LogoURL}}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
{{end}}
|
||||
<h3 class="vs-feature-title">{{.Name}}</h3>
|
||||
{{if .Description}}<p class="vs-feature-desc">{{.Description}}</p>{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- CTA Section -->
|
||||
{{if .Config.CTASection.Headline}}
|
||||
<section class="vs-cta-section" id="cta">
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<details class="item toggleable-item" {{if or .PageIsSettingsPagesGeneral .PageIsSettingsPagesBrand .PageIsSettingsPagesHero .PageIsSettingsPagesContent .PageIsSettingsPagesComparison .PageIsSettingsPagesSocial .PageIsSettingsPagesPricing .PageIsSettingsPagesFooter .PageIsSettingsPagesTheme .PageIsSettingsPagesLanguages}}open{{end}}>
|
||||
<details class="item toggleable-item" {{if or .PageIsSettingsPagesGeneral .PageIsSettingsPagesBrand .PageIsSettingsPagesHero .PageIsSettingsPagesContent .PageIsSettingsPagesComparison .PageIsSettingsPagesSocial .PageIsSettingsPagesPricing .PageIsSettingsPagesFooter .PageIsSettingsPagesTheme .PageIsSettingsPagesLanguages .PageIsSettingsPagesAdvanced}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "repo.settings.pages"}}</summary>
|
||||
<div class="menu">
|
||||
<a class="{{if .PageIsSettingsPagesGeneral}}active {{end}}item" href="{{.RepoLink}}/settings/pages">
|
||||
@@ -86,6 +86,9 @@
|
||||
<a class="{{if .PageIsSettingsPagesLanguages}}active {{end}}item" href="{{.RepoLink}}/settings/pages/languages">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.languages"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/pages/advanced">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.advanced"}}
|
||||
</a>
|
||||
</div>
|
||||
</details>
|
||||
<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables .PageIsActionsSettingsGeneral}}open{{end}}>
|
||||
|
||||
@@ -285,6 +285,15 @@
|
||||
<label for="push_mirror_sync_on_commit">{{ctx.Locale.Tr "repo.mirror_sync_on_commit"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Repository.HideDotfiles}}
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input id="push_mirror_exclude_hidden_files" name="push_mirror_exclude_hidden_files" type="checkbox" checked>
|
||||
<label for="push_mirror_exclude_hidden_files">{{ctx.Locale.Tr "repo.settings.mirror_settings.push_mirror.exclude_hidden_files"}}</label>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="inline field {{if .Err_PushMirrorInterval}}error{{end}}">
|
||||
<label for="push_mirror_interval">{{ctx.Locale.Tr "repo.mirror_interval" .MinimumMirrorInterval}}</label>
|
||||
<input id="push_mirror_interval" name="push_mirror_interval" value="{{if .push_mirror_interval}}{{.push_mirror_interval}}{{else}}{{.DefaultMirrorInterval}}{{end}}">
|
||||
|
||||
@@ -57,33 +57,6 @@
|
||||
<textarea name="custom_head" rows="4" placeholder="<meta ...>">{{.Config.Advanced.CustomHead}}</textarea>
|
||||
</div>
|
||||
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.app_stores"}}</h5>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>Google Play ID</label>
|
||||
<input name="google_play_id" value="{{.Config.Advanced.GooglePlayID}}" placeholder="com.example.app">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>App Store ID</label>
|
||||
<input name="app_store_id" value="{{.Config.Advanced.AppStoreID}}" placeholder="123456789">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inline fields">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="public_releases" {{if .Config.Advanced.PublicReleases}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.public_releases"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="hide_mobile_releases" {{if .Config.Advanced.HideMobileReleases}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.hide_mobile_releases"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
</div>
|
||||
<div class="ui horizontal divider">{{ctx.Locale.Tr "repo.settings.pages.brand_or"}}</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.brand_logo_url"}}</label>
|
||||
<input name="brand_logo_url" value="{{.Config.Brand.LogoURL}}" placeholder="https://example.com/logo.svg">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.brand_logo_url_help"}}</p>
|
||||
</div>
|
||||
@@ -68,6 +69,7 @@
|
||||
</div>
|
||||
<div class="ui horizontal divider">{{ctx.Locale.Tr "repo.settings.pages.brand_or"}}</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.brand_favicon_url"}}</label>
|
||||
<input name="brand_favicon_url" value="{{.Config.Brand.FaviconURL}}" placeholder="https://example.com/favicon.ico">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.brand_favicon_url_help"}}</p>
|
||||
</div>
|
||||
|
||||
@@ -124,6 +124,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.cross_promote_section"}}</h5>
|
||||
<div class="inline field">
|
||||
<div class="ui toggle checkbox">
|
||||
<input type="checkbox" name="cross_promote_enabled" {{if .Config.CrossPromote.Enabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.cross_promote_enabled_desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.cross_promote_headline"}}</label>
|
||||
<input name="cross_promote_headline" value="{{.Config.CrossPromote.Headline}}" placeholder="Related Offerings">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.cross_promote_subheadline"}}</label>
|
||||
<input name="cross_promote_subheadline" value="{{.Config.CrossPromote.Subheadline}}" placeholder="Explore our other projects">
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.cross_promote_help"}}</p>
|
||||
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.stats"}}</h5>
|
||||
<div id="stats-container">
|
||||
{{range $i, $stat := .Config.Stats}}
|
||||
@@ -142,6 +159,22 @@
|
||||
<button type="button" class="ui mini button" onclick="addStat()">+ Add Stat</button>
|
||||
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.value_props"}}</h5>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.label_value_props"}}</label>
|
||||
<input name="label_value_props" value="{{.Config.Navigation.LabelValueProps}}" placeholder="Why choose us">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.label_value_props_help"}}</p>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.value_props_headline"}}</label>
|
||||
<input name="value_props_headline" value="{{.Config.ValuePropsHeadline}}" placeholder="Built for makers">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.value_props_headline_help"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.value_props_subheadline"}}</label>
|
||||
<input name="value_props_subheadline" value="{{.Config.ValuePropsSubheadline}}" placeholder="A short tagline beneath the headline">
|
||||
</div>
|
||||
</div>
|
||||
<div id="valueprops-container">
|
||||
{{range $i, $vp := .Config.ValueProps}}
|
||||
<div class="fields valueprop-item">
|
||||
@@ -209,6 +242,22 @@
|
||||
<button type="button" class="ui mini button" onclick="addValueProp()">+ Add Value Prop</button>
|
||||
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.features"}}</h5>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.label_features"}}</label>
|
||||
<input name="label_features" value="{{.Config.Navigation.LabelFeatures}}" placeholder="Capabilities">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.label_features_help"}}</p>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.features_headline"}}</label>
|
||||
<input name="features_headline" value="{{.Config.FeaturesHeadline}}" placeholder="Everything you need">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.features_headline_help"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.features_subheadline"}}</label>
|
||||
<input name="features_subheadline" value="{{.Config.FeaturesSubheadline}}" placeholder="A short tagline beneath the headline">
|
||||
</div>
|
||||
</div>
|
||||
<div id="features-container">
|
||||
{{range $i, $f := .Config.Features}}
|
||||
<div class="fields feature-item">
|
||||
|
||||
@@ -35,9 +35,9 @@
|
||||
</div>
|
||||
<div class="ui horizontal divider">{{ctx.Locale.Tr "repo.settings.pages.hero_or"}}</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.image_url"}}</label>
|
||||
<input name="image_url" value="{{.Config.Hero.ImageURL}}" placeholder="https://example.com/hero.png">
|
||||
</div>
|
||||
<br>
|
||||
{{end}}
|
||||
|
||||
<div class="field">
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
{{if .PagesEnabled}}
|
||||
<div class="ui secondary pointing menu tw-mb-4">
|
||||
<a class="{{if .PageIsSettingsPagesGeneral}}active {{end}}item" href="{{.RepoLink}}/settings/pages">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.general"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesBrand}}active {{end}}item" href="{{.RepoLink}}/settings/pages/brand">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.brand"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesHero}}active {{end}}item" href="{{.RepoLink}}/settings/pages/hero">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.hero"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesContent}}active {{end}}item" href="{{.RepoLink}}/settings/pages/content">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.content"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesComparison}}active {{end}}item" href="{{.RepoLink}}/settings/pages/comparison">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.comparison"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesSocial}}active {{end}}item" href="{{.RepoLink}}/settings/pages/social">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.social"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesPricing}}active {{end}}item" href="{{.RepoLink}}/settings/pages/pricing">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.pricing"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesFooter}}active {{end}}item" href="{{.RepoLink}}/settings/pages/footer">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.footer"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesTheme}}active {{end}}item" href="{{.RepoLink}}/settings/pages/theme">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.theme"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesLanguages}}active {{end}}item" href="{{.RepoLink}}/settings/pages/languages">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.languages"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsPagesAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/pages/advanced">
|
||||
{{ctx.Locale.Tr "repo.settings.pages.advanced"}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user