Add and copy AGENTS.md for starter
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
. "git.gorlug.de/code/ersteller"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
. "git.gorlug.de/code/ersteller"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ tmp_dir = "tmp"
|
||||
bin = "./tmp/main"
|
||||
cmd = "time go build -gcflags=\"all=-l\" -ldflags=\"-w -s\" -o ./tmp/main main.go"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "cdk", "dist", "db", "static"]
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "cdk", "dist", "db", "static", "docker"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
# Development Guidelines
|
||||
|
||||
## Styling
|
||||
|
||||
Every page has styling in a separate CSS file.
|
||||
|
||||
- Page-specific styles should be placed in separate CSS files in the `static/` directory
|
||||
- Example: The media page has its styles in `static/media.css`
|
||||
- General/shared styles are in `static/styles.css`
|
||||
|
||||
## Single Page Implementation
|
||||
|
||||
### Path constants
|
||||
|
||||
- Define path constants for the root paths of the page only (not for sub-routes).
|
||||
|
||||
```go
|
||||
const MediaPath = "/media"
|
||||
const MediaPathDe = "/medien"
|
||||
```
|
||||
|
||||
### Page struct and initializer
|
||||
|
||||
- Define a `Page` struct that contains all routes and actions as fields.
|
||||
- Initialize routes in the `NewPage` constructor function.
|
||||
|
||||
```go
|
||||
type Page struct {
|
||||
// Routes
|
||||
ViewRoute HtmxRoute
|
||||
StartRoute HtmxRoute
|
||||
StopRoute HtmxRoute
|
||||
|
||||
}
|
||||
|
||||
func NewPage(server *Server) *Page {
|
||||
p := &Page{}
|
||||
|
||||
// Initialize routes
|
||||
p.ViewRoute = NewHtmxGetRoute(p.View, LanguagePaths{En: MediaPath, De: MediaPathDe}).SetActivePath(MediaPath)
|
||||
p.StartRoute = NewHtmxPostRoute(p.Start, LanguagePaths{En: MediaPath + "/start", De: MediaPathDe + "/start"})
|
||||
p.StopRoute = NewHtmxPostRoute(p.Stop, LanguagePaths{En: MediaPath + "/stop", De: MediaPathDe + "/stop"})
|
||||
|
||||
// Register routes
|
||||
p.ViewRoute.Add(server)
|
||||
p.StartRoute.Add(server)
|
||||
p.StopRoute.Add(server)
|
||||
|
||||
return p
|
||||
}
|
||||
```
|
||||
|
||||
### Multilingual content (`I18nText`)
|
||||
|
||||
- Declare user-facing text with `I18nText`; avoid hardcoded strings.
|
||||
- Create a separate `Texts` struct type and initialize it in a `createTexts()` function.
|
||||
|
||||
```go
|
||||
var texts *Texts
|
||||
|
||||
type Texts struct {
|
||||
PageTitle I18nText
|
||||
PageDescription I18nText
|
||||
HeroTitle I18nText
|
||||
StartProcessing I18nText
|
||||
}
|
||||
|
||||
func createTexts() {
|
||||
texts = &Texts{
|
||||
PageTitle: NewI18nText(map[Language]string{En: "Media Processing", De: "Medienverarbeitung"}),
|
||||
PageDescription: NewI18nText(map[Language]string{En: "Process and organize your media files", De: "Verarbeiten und organisieren Sie Ihre Mediendateien"}),
|
||||
HeroTitle: NewI18nText(map[Language]string{En: "Media Processing", De: "Medienverarbeitung"}),
|
||||
StartProcessing: NewI18nText(map[Language]string{En: "Start Processing", De: "Verarbeitung starten"}),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Page) getMetaData() PageWebsiteMetaData {
|
||||
return PageWebsiteMetaData{
|
||||
Title: texts.PageTitle,
|
||||
Lang: En,
|
||||
Description: texts.PageDescription,
|
||||
StyleSrcs: []string{"/static/media.css"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Page) View(c HtmxContext) {
|
||||
lang := c.GetLanguage()
|
||||
content := Group{
|
||||
Div(Class("hero"),
|
||||
H1(Text(texts.HeroTitle.FromLang(lang))),
|
||||
),
|
||||
}
|
||||
p.createPage(c, p.getMetaData(), content)
|
||||
}
|
||||
```
|
||||
|
||||
### Language-aware URLs and hx attributes
|
||||
|
||||
- Derive URLs from routes with the active language for all hx attributes.
|
||||
|
||||
```go
|
||||
lang := c.GetLanguage()
|
||||
|
||||
Button(
|
||||
Text(texts.StartProcessing.FromLang(lang)),
|
||||
p.StartRoute.GetHtmx(lang),
|
||||
hx.Target("body"),
|
||||
)
|
||||
|
||||
// Pagination example with hx.PushURL to keep browser URL in sync
|
||||
Button(
|
||||
Text(texts.Next.FromLang(lang)),
|
||||
hx.Get(p.RefreshRoute.ToUrlFromContext(c, lang)),
|
||||
hx.PushURL(p.ViewRoute.ToUrlFromContext(c, lang)),
|
||||
hx.Target("#queue-contents"),
|
||||
hx.Swap("innerHTML"),
|
||||
)
|
||||
```
|
||||
|
||||
### Page assembly and styles
|
||||
|
||||
- Build pages with `createPage` and supply metadata plus styles.
|
||||
- Create a `getMetaData()` function to centralize page metadata configuration.
|
||||
- Keep shared CSS in `static/styles.css` and page-specific CSS in `static/<page>.css`; include via `StyleSrcs`.
|
||||
|
||||
```go
|
||||
func (p *Page) getMetaData() PageWebsiteMetaData {
|
||||
return PageWebsiteMetaData{
|
||||
Title: texts.PageTitle,
|
||||
Lang: En,
|
||||
Description: texts.PageDescription,
|
||||
StyleSrcs: []string{"/static/media.css"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Page) View(c HtmxContext) {
|
||||
lang := c.GetLanguage()
|
||||
content := Group{
|
||||
Div(Class("hero"), H1(Text(texts.HeroTitle.FromLang(lang)))),
|
||||
// ... more components ...
|
||||
}
|
||||
|
||||
p.createPage(c, p.getMetaData(), content)
|
||||
}
|
||||
```
|
||||
|
||||
### HTMX partials and polling
|
||||
|
||||
- Use HTMX forms and partial refreshes; combine `hx.Target`, `hx.Swap`, and periodic polling (`hx.Trigger("every 5s")`) with language-aware URLs.
|
||||
|
||||
```go
|
||||
// Partial that refreshes every 5s when running
|
||||
Group{
|
||||
hx.Get(p.RefreshStatusRoute.ToUrlFromContext(c, lang)),
|
||||
hx.Trigger("every 5s"),
|
||||
hx.Target("#queue-status"),
|
||||
hx.Swap("innerHTML"),
|
||||
}
|
||||
|
||||
Form(
|
||||
p.SaveSettingsRoute.GetHtmx(lang),
|
||||
hx.Target("body"),
|
||||
// form fields...
|
||||
)
|
||||
|
||||
// Use GetHtmx() for form actions to ensure proper HTMX attributes
|
||||
Form(
|
||||
p.localLoginRoute.GetHtmx(language),
|
||||
hx.Target("body"),
|
||||
// form fields...
|
||||
)
|
||||
```
|
||||
|
||||
## HTMX Routes
|
||||
|
||||
**ALWAYS** prefer using `p.Route.GetHtmx()` over directly calling `hx.Get(p.Route.ToUrlFromContext())`.
|
||||
|
||||
**Example:**
|
||||
- ✅ Preferred: `p.RefreshStatusRoute.GetHtmx(c, lang)`
|
||||
- ❌ Avoid: `hx.Get(p.RefreshStatusRoute.ToUrlFromContext(c, lang))`
|
||||
|
||||
## Persisting Data with Ent Schemas
|
||||
|
||||
### Schema Definition
|
||||
|
||||
- Define schemas in `ent/schema/<entity>.go`
|
||||
- Each schema is a struct that embeds `ent.Schema`
|
||||
- Use the `TimeMixin` to automatically add `created_at` and `updated_at` fields
|
||||
|
||||
```go
|
||||
package schema
|
||||
|
||||
import (
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/schema/field"
|
||||
"entgo.io/ent/schema/edge"
|
||||
)
|
||||
|
||||
type Media struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
// Apply TimeMixin for automatic timestamp fields
|
||||
func (Media) Mixin() []ent.Mixin {
|
||||
return []ent.Mixin{
|
||||
TimeMixin{},
|
||||
}
|
||||
}
|
||||
|
||||
// Define fields
|
||||
func (Media) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.String("filename"),
|
||||
field.String("hash"),
|
||||
field.String("description").Default(""),
|
||||
field.Time("created_date").Optional().Nillable(),
|
||||
field.Int("year").Optional().Nillable(),
|
||||
field.Float("gps_latitude").Optional().Nillable(),
|
||||
}
|
||||
}
|
||||
|
||||
// Define relationships
|
||||
func (Media) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.To("tags", Tag.Type), // Many-to-Many
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Field Types and Options
|
||||
|
||||
**Common field types:**
|
||||
- `field.String("name")` - string field
|
||||
- `field.Int("count")` - integer field
|
||||
- `field.Float("latitude")` - float64 field
|
||||
- `field.Bool("active")` - boolean field
|
||||
- `field.Time("date")` - time.Time field
|
||||
|
||||
**Field modifiers:**
|
||||
- `.Default(value)` - set default value
|
||||
- `.Optional()` - field can be omitted on creation
|
||||
- `.Nillable()` - field can be nil (must be used with Optional)
|
||||
- `.Immutable()` - field cannot be updated after creation
|
||||
- `.Unique()` - field value must be unique
|
||||
|
||||
### Generating Code
|
||||
|
||||
After modifying schemas, regenerate the ent code:
|
||||
|
||||
```bash
|
||||
go run generate_schema.go
|
||||
```
|
||||
|
||||
### Usage in Handlers
|
||||
|
||||
**Create:**
|
||||
```go
|
||||
media, err := server.Client.Media.Create().
|
||||
SetFilename("photo.jpg").
|
||||
SetHash("abc123").
|
||||
SetDescription("Vacation photo").
|
||||
Save(c.Request.Context())
|
||||
```
|
||||
|
||||
**Query:**
|
||||
```go
|
||||
// Get by ID
|
||||
media, err := server.Client.Media.Get(c.Request.Context(), id)
|
||||
|
||||
// Query with filters
|
||||
medias, err := server.Client.Media.Query().
|
||||
Where(media.YearEQ(2024)).
|
||||
All(c.Request.Context())
|
||||
|
||||
// Query with edges
|
||||
medias, err := server.Client.Media.Query().
|
||||
WithTags(). // Load tags relationship
|
||||
All(c.Request.Context())
|
||||
```
|
||||
|
||||
**Update:**
|
||||
```go
|
||||
err := server.Client.Media.UpdateOneID(id).
|
||||
SetDescription("Updated description").
|
||||
Save(c.Request.Context())
|
||||
```
|
||||
|
||||
**Delete:**
|
||||
```go
|
||||
err := server.Client.Media.DeleteOneID(id).
|
||||
Exec(c.Request.Context())
|
||||
```
|
||||
|
||||
### Relationships (Edges)
|
||||
|
||||
**One-to-Many:**
|
||||
```go
|
||||
edge.To("posts", Post.Type) // User has many posts
|
||||
edge.From("owner", User.Type).Ref("posts").Unique() // Post belongs to one user
|
||||
```
|
||||
|
||||
**Many-to-Many:**
|
||||
```go
|
||||
edge.To("tags", Tag.Type) // Media has many tags
|
||||
edge.From("media", Media.Type).Ref("tags") // Tag has many media
|
||||
```
|
||||
|
||||
## Important Instructions
|
||||
|
||||
Do what has been asked; nothing more, nothing less.
|
||||
|
||||
**NEVER** create files unless they're absolutely necessary for achieving your goal.
|
||||
|
||||
**ALWAYS** prefer editing an existing file to creating a new one.
|
||||
|
||||
**NEVER** proactively create documentation files (`*.md`) or README files. Only create documentation files if explicitly requested by the User.
|
||||
@@ -2,8 +2,6 @@ package create
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "git.gorlug.de/code/ersteller"
|
||||
"git.gorlug.de/code/ersteller/starter/env"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -11,6 +9,9 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
. "git.gorlug.de/code/ersteller"
|
||||
"git.gorlug.de/code/ersteller/starter/env"
|
||||
)
|
||||
|
||||
type DatabaseType string
|
||||
@@ -97,6 +98,7 @@ func (s StarterCreator) Create() {
|
||||
}
|
||||
|
||||
s.copyFile("../.gitignore", ".gitignore")
|
||||
s.copyFile("../AGENTS.md", "AGENTS.md")
|
||||
|
||||
s.createEnvironment()
|
||||
directories := []string{
|
||||
|
||||
Reference in New Issue
Block a user