Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion internal/app/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,31 @@ func (a *entityTagFetcherAdapter) GetEntityTagsBatch(ctx context.Context, entity
for eid, tagList := range tagsMap {
infos := make([]entities.EntityTagInfo, len(tagList))
for i, t := range tagList {
infos[i] = entities.EntityTagInfo{Name: t.Name, Color: t.Color}
infos[i] = entities.EntityTagInfo{ID: t.ID, Name: t.Name, Color: t.Color}
}
result[eid] = infos
}
return result, nil
}

// GetEntityTags returns tags for a single entity.
func (a *entityTagFetcherAdapter) GetEntityTags(ctx context.Context, entityID string, includeDmOnly bool) ([]entities.EntityTagInfo, error) {
tagList, err := a.svc.GetEntityTags(ctx, entityID, includeDmOnly)
if err != nil {
return nil, err
}
infos := make([]entities.EntityTagInfo, len(tagList))
for i, t := range tagList {
infos[i] = entities.EntityTagInfo{ID: t.ID, Name: t.Name, Color: t.Color}
}
return infos, nil
}

// SetEntityTags sets the tags for a single entity.
func (a *entityTagFetcherAdapter) SetEntityTags(ctx context.Context, entityID string, campaignID string, tagIDs []int) error {
return a.svc.SetEntityTags(ctx, entityID, campaignID, tagIDs)
}

// entityCampaignCheckerAdapter wraps entities.EntityService to implement the
// sessions.EntityCampaignChecker interface, verifying entity-campaign membership
// to prevent cross-campaign IDOR attacks on entity linking.
Expand Down
13 changes: 13 additions & 0 deletions internal/plugins/admin/dashboard.templ
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,19 @@ templ AdminDashboardPage(userCount, campaignCount, mediaFileCount int, totalStor
<p class="text-xs text-fg-muted mt-1 group-hover:text-accent transition-colors">SMTP settings &rarr;</p>
</a>
</div>

<!-- Quick Actions -->
<div class="flex items-center gap-3 pt-2">
<a
href="/admin/design-lab"
target="_blank"
class="text-xs text-fg-muted hover:text-accent transition-colors inline-flex items-center gap-1.5"
>
<i class="fa-solid fa-palette text-[10px]"></i>
Open Demo Page
<i class="fa-solid fa-arrow-up-right-from-square text-[8px]"></i>
</a>
</div>
</div>
}
}
49 changes: 49 additions & 0 deletions internal/plugins/campaigns/form.templ
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,55 @@ templ CampaignCreateForm(csrfToken string, req *CreateCampaignRequest, errMsg st
</textarea>
</div>

<div>
<label for="genre" class="block text-sm font-medium text-fg-body mb-1">Campaign Genre</label>
<p class="text-xs text-fg-secondary mb-2">Choose a genre to get pre-configured entity types tailored to your setting.</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2" id="genre-picker" x-data="{ genre: '' }">
<label class="cursor-pointer">
<input type="radio" name="genre" value="" class="peer sr-only" x-model="genre" checked/>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg border border-edge text-sm peer-checked:border-accent peer-checked:bg-accent/5 hover:border-edge transition-colors">
<i class="fa-solid fa-hat-wizard text-fg-muted"></i>
<span class="text-fg">Standard</span>
</div>
</label>
<label class="cursor-pointer">
<input type="radio" name="genre" value="fantasy" class="peer sr-only" x-model="genre"/>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg border border-edge text-sm peer-checked:border-accent peer-checked:bg-accent/5 hover:border-edge transition-colors">
<i class="fa-solid fa-hat-wizard text-fg-muted"></i>
<span class="text-fg">Fantasy / D&amp;D</span>
</div>
</label>
<label class="cursor-pointer">
<input type="radio" name="genre" value="sci-fi" class="peer sr-only" x-model="genre"/>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg border border-edge text-sm peer-checked:border-accent peer-checked:bg-accent/5 hover:border-edge transition-colors">
<i class="fa-solid fa-rocket text-fg-muted"></i>
<span class="text-fg">Sci-Fi</span>
</div>
</label>
<label class="cursor-pointer">
<input type="radio" name="genre" value="horror" class="peer sr-only" x-model="genre"/>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg border border-edge text-sm peer-checked:border-accent peer-checked:bg-accent/5 hover:border-edge transition-colors">
<i class="fa-solid fa-ghost text-fg-muted"></i>
<span class="text-fg">Horror</span>
</div>
</label>
<label class="cursor-pointer">
<input type="radio" name="genre" value="modern" class="peer sr-only" x-model="genre"/>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg border border-edge text-sm peer-checked:border-accent peer-checked:bg-accent/5 hover:border-edge transition-colors">
<i class="fa-solid fa-city text-fg-muted"></i>
<span class="text-fg">Modern</span>
</div>
</label>
<label class="cursor-pointer">
<input type="radio" name="genre" value="historical" class="peer sr-only" x-model="genre"/>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg border border-edge text-sm peer-checked:border-accent peer-checked:bg-accent/5 hover:border-edge transition-colors">
<i class="fa-solid fa-landmark text-fg-muted"></i>
<span class="text-fg">Historical</span>
</div>
</label>
</div>
</div>

<div class="flex items-center space-x-4">
<button type="submit" class="btn-primary">Create Campaign</button>
<a href="/campaigns" class="btn-secondary">Cancel</a>
Expand Down
3 changes: 3 additions & 0 deletions internal/plugins/campaigns/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ type MailService interface {
// entities package directly.
type EntityTypeSeeder interface {
SeedDefaults(ctx context.Context, campaignID string) error
SeedGenre(ctx context.Context, campaignID string, genre string) error
}

// ContentTemplateSeeder seeds default content templates when a campaign is
Expand All @@ -643,6 +644,7 @@ type LayoutPresetSeeder interface {
type CreateCampaignRequest struct {
Name string `json:"name" form:"name"`
Description string `json:"description" form:"description"`
Genre string `json:"genre" form:"genre"`
}

// UpdateCampaignRequest holds the data submitted by the campaign edit form.
Expand Down Expand Up @@ -685,6 +687,7 @@ type UpdateSidebarConfigRequest struct {
type CreateCampaignInput struct {
Name string
Description string
Genre string // Optional genre preset for entity type seeding.
}

// UpdateCampaignInput is the validated input for updating a campaign.
Expand Down
16 changes: 11 additions & 5 deletions internal/plugins/campaigns/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,19 @@ func (s *campaignService) Create(ctx context.Context, userID string, input Creat
return nil, apperror.NewInternal(fmt.Errorf("adding owner member: %w", err))
}

// Seed default entity types for the new campaign.
// Seed entity types for the new campaign (genre-specific or defaults).
if s.seeder != nil {
if err := s.seeder.SeedDefaults(ctx, campaign.ID); err != nil {
// Non-fatal: campaign is still usable without default types.
slog.Warn("failed to seed default entity types",
var seedErr error
if input.Genre != "" {
seedErr = s.seeder.SeedGenre(ctx, campaign.ID, input.Genre)
} else {
seedErr = s.seeder.SeedDefaults(ctx, campaign.ID)
}
if seedErr != nil {
slog.Warn("failed to seed entity types",
slog.String("campaign_id", campaign.ID),
slog.Any("error", err),
slog.String("genre", input.Genre),
slog.Any("error", seedErr),
)
}
}
Expand Down
7 changes: 7 additions & 0 deletions internal/plugins/campaigns/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,13 @@ func (m *mockSeeder) SeedDefaults(ctx context.Context, campaignID string) error
return nil
}

func (m *mockSeeder) SeedGenre(ctx context.Context, campaignID string, genre string) error {
if m.seedDefaultsFn != nil {
return m.seedDefaultsFn(ctx, campaignID)
}
return nil
}

// --- Test Helpers ---

func newTestCampaignService(repo *mockCampaignRepo, users *mockUserFinder) CampaignService {
Expand Down
6 changes: 3 additions & 3 deletions internal/plugins/campaigns/show.templ
Original file line number Diff line number Diff line change
Expand Up @@ -343,14 +343,14 @@ templ defaultOwnerDashboard(cc *CampaignContext, recentEntities []RecentEntity,
</div>
</div>
</a>
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/plugins", cc.Campaign.ID)) } class="card p-4 hover:shadow-md hover:-translate-y-0.5 block group">
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/settings?tab=features", cc.Campaign.ID)) } class="card p-4 hover:shadow-md hover:-translate-y-0.5 block group">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-lg bg-green-50 dark:bg-green-900/30 flex items-center justify-center shrink-0">
<i class="fa-solid fa-puzzle-piece text-green-600 dark:text-green-400 text-sm"></i>
</div>
<div>
<h3 class="text-sm font-semibold text-fg group-hover:text-accent transition-colors">Plugins</h3>
<p class="text-[11px] text-fg-muted">Features & addons</p>
<h3 class="text-sm font-semibold text-fg group-hover:text-accent transition-colors">Features</h3>
<p class="text-[11px] text-fg-muted">Enable campaign features</p>
</div>
</div>
</a>
Expand Down
6 changes: 3 additions & 3 deletions internal/plugins/designlab/design_lab.templ
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import "github.com/keyxmakerx/chronicle/internal/templates/layouts"

// DesignLabPage renders the component showcase for previewing UI variants.
templ DesignLabPage() {
@layouts.App("Design Lab - Admin") {
@layouts.App("Demo Page - Admin") {
<div class="max-w-6xl mx-auto space-y-4" x-data="{ tab: 'buttons' }">
<div class="flex items-center justify-between">
<h1 class="text-xl font-bold text-fg">
<i class="fa-solid fa-palette mr-2 text-accent"></i>Design Lab
<i class="fa-solid fa-palette mr-2 text-accent"></i>Demo Page
</h1>
<span class="text-xs text-fg-muted">Component showcase &mdash; changes here don&apos;t affect the live site</span>
<span class="text-xs text-fg-muted">Component showcase &mdash; preview all UI components in one place</span>
</div>

<!-- Tabs -->
Expand Down
Loading
Loading