321 lines
7.9 KiB
Markdown
321 lines
7.9 KiB
Markdown
---
|
|
apply: always
|
|
---
|
|
|
|
# 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.
|