21 Commits

Author SHA1 Message Date
ef323c291f chore(cd): hardening of env settings
All checks were successful
deploy-socialize / image (push) Successful in 28s
deploy-socialize / deploy (push) Successful in 15s
2026-05-06 21:25:11 -04:00
4eb0fbc22b fix: avoid feedback screenshot concurrency save
All checks were successful
deploy-socialize / image (push) Successful in 33s
deploy-socialize / deploy (push) Successful in 19s
2026-05-06 20:14:22 -04:00
afe22949c5 ci: run deploy job on ubuntu runner
All checks were successful
deploy-socialize / image (push) Successful in 29s
deploy-socialize / deploy (push) Successful in 23s
2026-05-06 16:20:59 -04:00
ebb87b286f ci: checkout deploy compose artifact
Some checks failed
deploy-socialize / image (push) Successful in 32s
deploy-socialize / deploy (push) Failing after 2s
2026-05-06 16:18:40 -04:00
f1da3a44de ci: sync production compose file
Some checks failed
deploy-socialize / image (push) Successful in 29s
deploy-socialize / deploy (push) Failing after 7s
2026-05-06 16:16:55 -04:00
419dbf0185 ci: align compose database host
All checks were successful
deploy-socialize / image (push) Successful in 34s
deploy-socialize / deploy (push) Successful in 14s
2026-05-06 15:56:29 -04:00
909ae6f092 ci: export backend deployment environment
All checks were successful
deploy-socialize / image (push) Successful in 34s
deploy-socialize / deploy (push) Successful in 14s
2026-05-06 15:49:28 -04:00
a97ff2dc38 fix: add verification resend flow
All checks were successful
deploy-socialize / image (push) Successful in 1m21s
deploy-socialize / deploy (push) Successful in 14s
2026-05-06 15:43:25 -04:00
7a862a202a fix: normalize Resend API key configuration
All checks were successful
deploy-socialize / image (push) Successful in 57s
deploy-socialize / deploy (push) Successful in 23s
2026-05-06 15:36:49 -04:00
1ae3188d34 chore: configure preprod email secrets
All checks were successful
deploy-socialize / image (push) Successful in 52s
deploy-socialize / deploy (push) Successful in 13s
2026-05-06 15:24:17 -04:00
fb7811c469 ci: quote deploy environment secrets
All checks were successful
deploy-socialize / image (push) Successful in 27s
deploy-socialize / deploy (push) Successful in 13s
2026-05-06 15:08:53 -04:00
0a6d730ca0 chore: source compose database password from secrets
Some checks failed
deploy-socialize / image (push) Successful in 30s
deploy-socialize / deploy (push) Failing after 6s
2026-05-06 15:05:10 -04:00
d2d3bee975 ci: remove repository hygiene check 2026-05-06 14:51:43 -04:00
78de068cd1 chore: ignore AI agent local state 2026-05-06 14:50:06 -04:00
1965dc2c9e docs: remove archived legacy material
All checks were successful
deploy-socialize / image (push) Successful in 1m24s
deploy-socialize / deploy (push) Successful in 9s
2026-05-06 14:40:28 -04:00
f0d635ef21 chore: remove legacy Hutopy assets 2026-05-06 14:36:23 -04:00
d59d667796 chore: remove legacy deployment domains 2026-05-06 14:33:34 -04:00
5c0e40db7e feat: centralize frontend branding 2026-05-06 14:27:09 -04:00
dc9a980958 fix: frontend API base URL
All checks were successful
deploy-socialize / image (push) Successful in 1m26s
deploy-socialize / deploy (push) Successful in 8s
2026-05-06 10:56:59 -04:00
c40653b2b7 chore(ci): guards against tracked build artefacts
All checks were successful
deploy-socialize / deploy (push) Successful in 8s
deploy-socialize / image (push) Successful in 32s
2026-05-05 23:41:20 -04:00
f240d32ce6 chore(ci): use app Caddyfile in frontend image
All checks were successful
deploy-socialize / image (push) Successful in 55s
deploy-socialize / deploy (push) Successful in 8s
2026-05-05 23:37:25 -04:00
84 changed files with 585 additions and 972 deletions

View File

@@ -24,7 +24,7 @@ jobs:
-t git.mapachotes.com/jbourdon/socialize-api:latest \
-f backend/src/Socialize.Api/Dockerfile .
docker build \
--build-arg VITE_API_URL=/api \
--build-arg VITE_API_URL=/ \
-t git.mapachotes.com/jbourdon/socialize-web:${{ gitea.sha }} \
-t git.mapachotes.com/jbourdon/socialize-web:latest \
-f frontend/Dockerfile .
@@ -37,8 +37,9 @@ jobs:
deploy:
needs: image
runs-on: bookworm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install SSH client
run: apt-get update && apt-get install -y openssh-client
- name: Deploy on sobina
@@ -46,9 +47,29 @@ jobs:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_SSH_PRIVATE_KEY_B64: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY_B64 }}
SOCIALIZE_IMAGE_TAG: ${{ gitea.sha }}
run: |
: "${SOCIALIZE_IMAGE_TAG:?SOCIALIZE_IMAGE_TAG is required}"
mkdir -p ~/.ssh
printf '%s' "$DEPLOY_SSH_PRIVATE_KEY_B64" | base64 -d > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
write_env_value() {
key="$1"
value="$2"
escaped_value="$(printf '%s' "$value" | sed "s/'/'\\\\''/g")"
printf "%s='%s'\n" "$key" "$escaped_value"
}
deploy_env="$(mktemp)"
{
write_env_value SOCIALIZE_IMAGE_TAG "$SOCIALIZE_IMAGE_TAG"
} > "$deploy_env"
scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new "$deploy_env" "$DEPLOY_USER@$DEPLOY_HOST:/srv/prod/socialize/.deploy.env"
rm -f "$deploy_env"
scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new deploy/compose.yml "$DEPLOY_USER@$DEPLOY_HOST:/srv/prod/socialize/compose.yml"
ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST" \
'cd /srv/prod/socialize && ./deploy.sh'
'test -r /etc/socialize/socialize.env && cd /srv/prod/socialize && ./deploy.sh'

10
.gitignore vendored
View File

@@ -22,6 +22,10 @@ Thumbs.db
# .NET
bin/
obj/
**/[Bb]in/
**/[Oo]bj/
**/[Bb]in[\\]*
**/[Oo]bj[\\]*
TestResults/
# Node
@@ -30,6 +34,7 @@ dist/
.vite/
# Local environment files
.env
*.local
.env.local
.env.*.local
@@ -38,5 +43,8 @@ App_Data/
# Local SSL certificates
*.pem
# Ai
# AI agent local state
.agents
.agents/
.codex
.codex/

View File

@@ -76,6 +76,12 @@ http://localhost:8080
http://<this-machine-lan-ip>:8080
```
For preprod deployment, configure the `POSTGRES_PASSWORD`, `RESEND_API_KEY`,
`RESEND_FROM_EMAIL`, and `JWT_SIGNING_KEY` Gitea secrets.
The deploy workflow writes the remote `.env` file and syncs `deploy/compose.yml`
before running the server deploy script.
Use the raw Resend API key value for `RESEND_API_KEY`, without a `Bearer ` prefix.
## Solution
```bash

View File

@@ -70,7 +70,6 @@ public static class DependencyInjection
{
authenticationBuilder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
{
jwtBearerOptions.Authority = "https://hutopy.com";
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,

View File

@@ -1,5 +0,0 @@
<wpf:ResourceDictionary xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xml:space="preserve">
<s:Boolean x:Key="/Default/UserDictionary/Words/=hutopy/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -26,8 +26,14 @@ public static class DependencyInjection
builder.Services.Configure<EmailerOptions>(
builder.Configuration.GetSection(EmailerOptions.ConfigurationSection));
builder.Services.AddTransient<IEmailSender, ResendEmailSender>();
//builder.Services.AddTransient<IEmailSender, EmailSender>();
if (builder.Environment.IsDevelopment())
{
builder.Services.AddTransient<IEmailSender, LoggerEmailSender>();
}
else
{
builder.Services.AddTransient<IEmailSender, ResendEmailSender>();
}
builder.Services.AddHttpClient();

View File

@@ -5,18 +5,15 @@ namespace Socialize.Api.Infrastructure.Emailer.Services;
public class LoggerEmailSender(ILogger<IEmailSender> logger)
: IEmailSender
{
public async Task SendEmailAsync(string email, string subject, string message)
public Task SendEmailAsync(string email, string subject, string message)
{
try
{
logger.LogInformation("Sending email to {Email} with subject: {Subject}", email, subject);
await Task.Delay(1000);
logger.LogInformation("Email sent successfully to {Email}", email);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to send email to {Email}", email);
throw;
}
logger.LogInformation(
"Development email to {Email} with subject {Subject}:{NewLine}{Message}",
email,
subject,
Environment.NewLine,
message);
return Task.CompletedTask;
}
}

View File

@@ -20,8 +20,24 @@ public class ResendEmailSender : IEmailSender
_httpClient = httpClientFactory.CreateClient();
_options = options.Value;
string apiKey = NormalizeApiKey(_options.ApiKey);
string fromEmail = _options.FromEmail?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(apiKey))
{
throw new InvalidOperationException("Emailer:ApiKey is required when using Resend email delivery.");
}
if (string.IsNullOrWhiteSpace(fromEmail))
{
throw new InvalidOperationException("Emailer:FromEmail is required when using Resend email delivery.");
}
_options.ApiKey = apiKey;
_options.FromEmail = fromEmail;
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", _options.ApiKey);
new AuthenticationHeaderValue("Bearer", apiKey);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
@@ -43,4 +59,16 @@ public class ResendEmailSender : IEmailSender
$"Resend email failed: {response.StatusCode} - {body}");
}
}
private static string NormalizeApiKey(string? apiKey)
{
string normalized = apiKey?.Trim().Trim('"', '\'') ?? string.Empty;
const string bearerPrefix = "Bearer ";
if (normalized.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase))
{
normalized = normalized[bearerPrefix.Length..].Trim();
}
return normalized;
}
}

View File

@@ -38,8 +38,7 @@ public class AttachMyFeedbackScreenshotHandler(
Guid reporterUserId = User.GetUserId();
FeedbackReport? report = await dbContext.FeedbackReports
.Include(candidate => candidate.Tags)
.Include(candidate => candidate.Screenshot)
.AsNoTracking()
.SingleOrDefaultAsync(
candidate => candidate.Id == id && candidate.ReporterUserId == reporterUserId,
ct);
@@ -50,7 +49,11 @@ public class AttachMyFeedbackScreenshotHandler(
return;
}
if (report.Screenshot is not null)
bool hasScreenshot = await dbContext.FeedbackScreenshots
.AsNoTracking()
.AnyAsync(candidate => candidate.FeedbackReportId == report.Id, ct);
if (hasScreenshot)
{
AddError("A screenshot is already attached to this feedback report.");
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
@@ -94,7 +97,7 @@ public class AttachMyFeedbackScreenshotHandler(
}
DateTimeOffset now = DateTimeOffset.UtcNow;
report.Screenshot = new FeedbackScreenshot
FeedbackScreenshot screenshot = new()
{
Id = screenshotId,
FeedbackReportId = report.Id,
@@ -105,10 +108,45 @@ public class AttachMyFeedbackScreenshotHandler(
BlobName = blobName,
CreatedAt = now,
};
report.LastActivityAt = now;
await dbContext.SaveChangesAsync(ct);
await SendOkAsync(report.ToDto(), ct);
await using Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction transaction =
await dbContext.Database.BeginTransactionAsync(ct);
dbContext.FeedbackScreenshots.Add(screenshot);
try
{
await dbContext.SaveChangesAsync(ct);
}
catch (DbUpdateException)
{
await transaction.RollbackAsync(ct);
AddError("A screenshot is already attached to this feedback report.");
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
return;
}
int updatedRows = await dbContext.FeedbackReports
.Where(candidate => candidate.Id == report.Id && candidate.ReporterUserId == reporterUserId)
.ExecuteUpdateAsync(
setters => setters.SetProperty(candidate => candidate.LastActivityAt, now),
ct);
if (updatedRows == 0)
{
await transaction.RollbackAsync(ct);
await SendNotFoundAsync(ct);
return;
}
await transaction.CommitAsync(ct);
FeedbackReport responseReport = await dbContext.FeedbackReports
.AsNoTracking()
.Include(candidate => candidate.Tags)
.Include(candidate => candidate.Screenshot)
.SingleAsync(candidate => candidate.Id == report.Id, ct);
await SendOkAsync(responseReport.ToDto(), ct);
}
private static string NormalizeFileName(string? fileName, string extension)

View File

@@ -32,10 +32,18 @@ public class RegisterHandler(
RegisterRequest request,
CancellationToken ct)
{
// Check if the user already exists
User? existingUser = await userManager.FindByEmailAsync(request.Email);
if (existingUser is not null)
{
if (!existingUser.EmailConfirmed)
{
await emailVerificationService.SendVerificationEmailAsync(existingUser);
await SendOkAsync(
new RegisterResponse("Registration successful! Please check your email to verify your account."),
ct);
return;
}
await SendStringAsync(
"A user with this email already exists",
400,

View File

@@ -2,9 +2,6 @@
"Stripe": {
"SocializeRate": 0.05
},
"Website": {
"FrontendBaseUrl": "https://socialize.mapachotes.com"
},
"LocalBlobStorage": {
"RootPath": "App_Data/blob-storage",
"RequestPath": "/api/storage"

61
deploy/compose.yml Normal file
View File

@@ -0,0 +1,61 @@
services:
db:
image: postgres:16
restart: unless-stopped
env_file:
- /etc/socialize/socialize.env
- .deploy.env
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 20
networks:
- internal
api:
image: git.mapachotes.com/jbourdon/socialize-api:${SOCIALIZE_IMAGE_TAG}
restart: unless-stopped
env_file:
- /etc/socialize/socialize.env
- .deploy.env
environment:
ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT}
ASPNETCORE_URLS: ${ASPNETCORE_URLS}
ConnectionStrings__PostgresConnection: Host=${POSTGRES_HOST};Port=${POSTGRES_PORT};Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}
Website__FrontendBaseUrl: ${WEBSITE_FRONTEND_BASE_URL}
Emailer__ApiKey: ${RESEND_API_KEY}
Emailer__FromEmail: ${RESEND_FROM_EMAIL}
Authentication__Jwt__Issuer: ${JWT_ISSUER}
Authentication__Jwt__Audience: ${JWT_AUDIENCE}
Authentication__Jwt__Key: ${JWT_SIGNING_KEY}
Authentication__Jwt__Lifetime: ${JWT_LIFETIME}
Authentication__Jwt__RefreshTokenLifetime: ${JWT_REFRESH_TOKEN_LIFETIME}
depends_on:
db:
condition: service_healthy
volumes:
- ./blob-storage:/app/App_Data/blob-storage
expose:
- "8080"
networks:
- internal
web:
image: git.mapachotes.com/jbourdon/socialize-web:${SOCIALIZE_IMAGE_TAG}
restart: unless-stopped
depends_on:
- api
ports:
- "127.0.0.1:8080:80"
networks:
- internal
networks:
internal:

View File

@@ -1,10 +1,10 @@
services:
postgres:
db:
image: postgres:latest
environment:
POSTGRES_USER: sa
POSTGRES_PASSWORD: P@ssword123!
POSTGRES_DB: socialize
POSTGRES_USER: ${POSTGRES_USER:-sa}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
POSTGRES_DB: ${POSTGRES_DB:-socialize}
ports:
- "5433:5432"
healthcheck:
@@ -18,11 +18,19 @@ services:
context: .
dockerfile: backend/src/Socialize.Api/Dockerfile
environment:
ASPNETCORE_ENVIRONMENT: Development
ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Development}
ASPNETCORE_URLS: http://0.0.0.0:8080
ConnectionStrings__PostgresConnection: Host=postgres;Port=5432;Database=socialize;Username=sa;Password=P@ssword123!
ConnectionStrings__PostgresConnection: Host=${POSTGRES_HOST:-db};Port=${POSTGRES_PORT:-5432};Database=${POSTGRES_DB:-socialize};Username=${POSTGRES_USER:-sa};Password=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
Website__FrontendBaseUrl: ${WEBSITE_FRONTEND_BASE_URL:-http://localhost:8080}
Emailer__ApiKey: ${RESEND_API_KEY:-}
Emailer__FromEmail: ${RESEND_FROM_EMAIL:-}
Authentication__Jwt__Issuer: ${JWT_ISSUER:-http://localhost:8080}
Authentication__Jwt__Audience: ${JWT_AUDIENCE:-socialize-local}
Authentication__Jwt__Key: ${JWT_SIGNING_KEY:-socialize-dev-local-signing-key-please-change}
Authentication__Jwt__Lifetime: ${JWT_LIFETIME:-00:05:00}
Authentication__Jwt__RefreshTokenLifetime: ${JWT_REFRESH_TOKEN_LIFETIME:-0.00:30:00}
depends_on:
postgres:
db:
condition: service_healthy
expose:
- "8080"

View File

@@ -28,13 +28,6 @@ This folder contains the project documentation used to guide product, implementa
- [FEATURES/organizations.md](/home/jbourdon/repos/social-media/docs/FEATURES/organizations.md): organization account boundary, membership, billing, connectors, and workspace ownership.
- [BACKLOG.md](/home/jbourdon/repos/social-media/docs/BACKLOG.md): deferred technical and product work.
## Archived
- [archive/PLAN.md](/home/jbourdon/repos/social-media/docs/archive/PLAN.md): early pivot plan.
- [archive/SOCIALIZE.md](/home/jbourdon/repos/social-media/docs/archive/SOCIALIZE.md): original consolidated product brief.
- [archive/Stripe.md](/home/jbourdon/repos/social-media/docs/archive/Stripe.md): legacy Hutopy monetization notes.
- [archive/WORKSHEET.md](/home/jbourdon/repos/social-media/docs/archive/WORKSHEET.md): historical transition worksheet.
## Root Docs
- [README.md](/home/jbourdon/repos/social-media/README.md): repository overview and local development setup.

View File

@@ -0,0 +1,26 @@
# Task: Add global frontend branding configuration
## Goal
Centralize product branding for the frontend so product name, visible brand marks, brand assets, and theme colors can be changed from one module instead of being hardcoded across shell and auth surfaces.
## Relevant Files
- `frontend/src/branding/branding.js`
- `frontend/src/branding/applyBranding.js`
- `frontend/src/components/branding/BrandMark.vue`
- `frontend/src/components/branding/BrandLogo.vue`
- `frontend/src/main.js`
- `frontend/src/assets/main.css`
- `frontend/src/layouts/main/AppSidebar.vue`
- `frontend/src/static/components/LandingSiteMenu.vue`
- `frontend/src/features/auth/views/RegisterView.vue`
- `frontend/src/features/auth/views/LoginView.vue`
- `frontend/public/images/brand/*`
## Validation
```bash
cd frontend
npm run build
```

View File

@@ -1,431 +0,0 @@
# PLAN
> Historical planning snapshot. This document reflects the early product-pivot plan and is not the source of truth for the current implementation state.
## Purpose
This document defines the build plan to close the gap between the current codebase and the target product described in [SOCIALIZE.md](/home/jbourdon/repos/social-media/docs/archive/SOCIALIZE.md).
The current repository is a dead `Hutopy` codebase. The target product, temporarily named `Socialize`, is a workflow application for social media content review, revision, approval, and readiness for publication.
This is a full product pivot, not a gradual feature expansion.
## Goal
Build a product that becomes the system of workflow for:
- internal content review
- provider collaboration
- client approval
- version tracking
- audit trail
- notification-driven progress
- publication readiness handoff
The first version should solve the approval pain cleanly before deeper integrations or scheduling features are added.
## Gap Summary
### Current Codebase
The current repository contains:
- backend modular structure with FastEndpoints and Entity Framework Core
- authentication and infrastructure foundations
- file storage patterns
- frontend Vue application shell
- legacy business modules centered on creators, tipping, memberships, and creator-facing experiences
### Target Product
The target product needs:
- workspace and client management
- provider and internal team collaboration
- content item lifecycle management
- asset and revision tracking
- comments and approval workflow
- workflow events and notifications
- client-facing review portal
- Google Drive-centered asset ownership
- billing readiness for a future Software as a Service offering
### Core Gap
The codebase has technical scaffolding that can be reused, but the business domain is largely wrong for the target product.
The main gap is not infrastructure. The main gap is domain shape, frontend surface, and workflow behavior.
## Build Principles
1. Reuse technical foundations when they help, but do not keep old business concepts for convenience.
2. Keep the modular backend structure.
3. Prefer clean domain language from day one over transitional naming.
4. Build around Google Drive ownership first, not direct-upload-first assumptions.
5. Deliver one vertical workflow before broad integrations.
6. Remove legacy Hutopy product concepts early to reduce semantic drag.
7. Keep changes reviewable and validate each phase before widening scope.
## Keep, Replace, Remove
### Keep
- ASP.NET Core backend shell
- FastEndpoints setup
- modular registration pattern
- Entity Framework Core with per-module context where useful
- infrastructure wiring
- authentication foundation if it can be adapted cleanly
- payment infrastructure patterns and Stripe integration capability for future billing
- blob or file-handling patterns that still apply
- Vue, Vite, Pinia, router, and API client shell
- deployment workflows if still useful for the new product
### Replace
- domain module set
- route design
- data model
- frontend information architecture
- user flows
- branding and product language
### Remove
- creator domain concepts
- creator public profile features
- creator monetization flows
- Hutopy naming across code and frontend copy
- dead UI components and stores tied only to the old product
### Retire And Rebuild
- old tipping business flows
- old membership business flows
- creator-specific Stripe onboarding flows
- payment routes and models that are tightly coupled to the dead Hutopy business model
Keep the underlying payment capability, but rebuild the business-facing billing model around the new product when pricing and subscription design are ready.
## Target Product Scope For Version 1
Version 1 should own this workflow:
1. Internal team creates a content item.
2. Assets are linked from Google Drive.
3. Publication message and metadata are attached.
4. Internal reviewer or provider receives a request.
5. Comments and revisions happen in one place.
6. Client receives a review request.
7. Client approves, rejects, or requests changes.
8. Item becomes ready for publishing handoff.
Not version 1:
- full scheduling engine
- full social publishing
- advanced third-party synchronization
- analytics suite
- full digital asset management platform
- full creative production tooling
- customer billing and subscription management user flows
## Target Backend Module Map
Recommended backend modules:
- `Identity`
- users, authentication, internal roles
- `Workspaces`
- agencies or operating teams
- `Clients`
- brands, creators, businesses served by a workspace
- `Providers`
- external production partners
- `Projects`
- grouped bodies of work for a client, which may later contain campaign concepts if needed
- `ContentItems`
- reviewable units with metadata, copy, due dates, and status
- `Assets`
- asset references, Google Drive linkage, versions, previews
- `Approvals`
- review requests, approvers, decisions, status transitions
- `Comments`
- threads, replies, resolution state, contextual discussion
- `Notifications`
- workflow events, reminders, delivery preferences
Possible later modules:
- `Campaigns`
- `Calendars`
- `Integrations`
- `Publishing`
- `Analytics`
- `Billing`
## Target Frontend Surface
Recommended frontend areas:
- authentication
- workspace switcher or workspace context
- client list
- project list
- review queue dashboard
- content item detail page
- revision and comment timeline
- approval status panel
- notification center
- external client review portal
Later frontend areas:
- calendar view
- integration settings
- publishing handoff dashboard
- workflow analytics
## Proposed Data Backbone
### Content Item
A content item should carry:
- title
- publication message
- notes
- project
- client
- creator or brand context where relevant
- publication targets
- publication dates by network when relevant
- due date
- current status
- current active revision set
### Asset
An asset should carry:
- asset type
- source type
- Google Drive file reference
- preview metadata
- current version
- version history
### Approval Request
An approval request should carry:
- target content item
- requested reviewers
- stage type
- sent at
- due at
- decision state
- decision history
### Notification Event
A notification event should carry:
- event type
- triggered by
- target entity
- recipients
- delivery status
- timestamps
## Execution Strategy
### Phase 0: Freeze Product Direction
Deliverables:
- validated worksheet in English
- optional French mirror of the worksheet
- agreed module map
- agreed version 1 scope and anti-scope
Exit criteria:
- no ambiguity about the first workflow to build
- no unresolved disagreement about Google Drive ownership
### Phase 1: Remove Legacy Product Surface
Goals:
- reduce confusion from Hutopy concepts
- make the codebase ready for the new domain
Work:
- remove or retire legacy frontend views tied to creators, tipping, and memberships
- remove legacy backend modules that are clearly dead
- rename product-facing strings, assets, and configuration references
- identify infrastructure pieces that stay
- preserve Stripe and payment infrastructure that can support future Software as a Service billing
- identify backend modules that should be replaced instead of adapted
Exit criteria:
- codebase no longer communicates the old product direction
- remaining code clearly supports reuse or is queued for replacement
### Phase 2: Establish New Domain Skeleton
Goals:
- introduce the new product vocabulary into code
- prepare clean module boundaries
Work:
- create new backend modules
- define initial entities and contexts
- wire modules in `Program.cs`
- define route namespaces and tags
- create frontend route skeleton for the new product
- define new stores for auth, workspace context, review queue, and content items
Exit criteria:
- application compiles with the new module map in place
- legacy domain is no longer the center of the app
### Phase 3: Build The First Vertical Slice
Vertical slice:
- create content item
- link Google Drive asset
- add publication message and publication target data
- request internal review
- comment on item
- request changes
- upload or register a new revision
- request client review
- approve from client portal
- mark ready to publish
Backend work:
- commands and queries for content item creation and retrieval
- asset linkage and versioning
- comment creation and retrieval
- approval request and decision endpoints
- status transition logic
- workflow event emission
Frontend work:
- content item creation flow
- content item detail view
- comments panel
- approval action UI
- status timeline
- simple client review page
Exit criteria:
- one item can move end-to-end from draft to approved
- all actions are traceable in one place
### Phase 4: Add Notification Backbone
Goals:
- make workflow movement visible without manual follow-up
Work:
- define notification event types
- trigger events on comments, revisions, requests, and decisions
- add email notifications
- add in-app notification center or lightweight feed
- add reminder jobs for pending reviews
Exit criteria:
- users are informed when workflow events occur
- delayed approvals can be followed without manual chasing
### Phase 5: Harden Version 1 For Real Usage
Goals:
- make the workflow usable for the first real client
Work:
- permissions and role hardening
- validation and error handling
- audit trail review
- filtering and dashboard improvements
- comment resolution
- required approver logic if needed
- publication dates by network support
- quality pass on mobile review experience
Exit criteria:
- the first client can run a real approval cycle in the product
### Phase 6: Evaluate Phase 2 Expansion
Candidates:
- calendar visibility
- Google Drive sync improvements
- Canva linkage
- MailChimp approval path
- scheduler handoff integrations
- billing and subscription management for the Software as a Service offer
Rule:
Only start these after version 1 workflow is demonstrably useful.
## Immediate Technical Tasks
Recommended next implementation tasks:
1. Rename the product and remove visible Hutopy branding.
2. Inventory which backend modules are deleted versus replaced.
3. Define the new backend module directories and initial project structure.
4. Replace the frontend router with the new application surface.
5. Model `ContentItem`, `Asset`, `AssetVersion`, `ApprovalRequest`, `ApprovalDecision`, `CommentThread`, and `NotificationEvent`.
6. Implement the first vertical slice end to end.
## Risks
- scope creep into scheduling and publishing too early
- forcing the new domain into old creator-centric structures
- under-designing workflow status and revision semantics
- overbuilding integrations before the core workflow is proven
- making external review too heavy for clients
## Validation Checklist
Before claiming version 1 readiness:
- a workspace can manage at least one client
- a content item can include publication message and publication targets
- assets can be linked from Google Drive
- internal review can request changes
- client review can approve or reject
- status history is visible
- notification events are triggered
- the latest approved state is clear
## Working Note
This plan should be updated as soon as the first implementation decisions are made, especially:
- exact module names
- exact database boundaries
- whether `Providers` stands alone or is modeled as a participant role
- whether notifications are their own module or an infrastructure concern

View File

@@ -1,328 +0,0 @@
# Social Media Approval Workflow
Temporary product name: `Socialize`
## Project Intent
Build `Socialize`, an application that replaces the current approval process based on Google Drive, phone calls, emails, and spreadsheets.
The product is not a public social network. It is an internal/external workflow tool for content review, feedback, approval, and publication readiness.
## Shared Vocabulary
- Approval workflow: the end-to-end process from draft creation to final approval.
- Content item: the reviewable unit that bundles assets, publication message or copy, dates, and channel targets.
- Asset: a file attached to a content item, such as a video, image, or document.
- Revision: a new version of an asset or copy after feedback.
- External reviewer: a client or partner who reviews content without being part of the internal team.
- Provider: an external production partner, such as a film crew, photographer, editor, or designer, who may deliver drafts and receive change requests.
- Software as a Service (SaaS): a cloud-based product used through the web, such as Canva, MailChimp, HootSuite, or Metricool.
- Minimum Viable Product (MVP): the smallest product version that solves the main pain point well enough to validate the market.
- Service Level Agreement (SLA): an agreed service target, such as a review deadline or escalation threshold.
## Problem Statement
Social media managers and production teams currently manage content approvals manually:
- Assets are stored in Google Drive.
- The social media manager may have back-and-forth with both upstream providers and downstream clients.
- Feedback is exchanged by phone, email, messages, and spreadsheets.
- Version history is unclear.
- It is hard to know which file is the latest one.
- Comments are scattered across multiple channels.
- Internal approvals and client approvals follow similar patterns but are not centralized.
- Follow-ups are manual, so approvals get delayed.
Result: too much back-and-forth, poor traceability, avoidable delays, and risk of publishing the wrong asset or outdated copy.
## Existing Tools Observed
- Google Drive for videos, images, calendars, and documents
- Google Sheets or similar for tracking comments and status
- Phone and email for review/approval conversations
- HootSuite
- Metricool
- Canva
- MailChimp
## Primary Users
- Social media manager
- Account manager / customer success
- Client approver
- External provider / production partner
- Internal producer
- Internal employee / content contributor
- Administrator
## Core Use Cases
### 1. Client Approval Workflow
A social media manager prepares content for a client and sends it for approval.
The client should be able to:
- view the content package
- preview files
- read captions, descriptions, and project notes
- leave comments
- request changes
- approve or reject
The team should be able to:
- see approval status in real time
- answer comments in context
- upload revised versions
- keep a clear audit trail of who said what and when
- know exactly which version is approved
### 2. Internal Production Workflow
The same workflow should work internally for producers, employees, and external production partners before the content is shown to the client or scheduled for publishing.
Example:
- contributor uploads draft
- external provider can upload draft or revised media
- producer reviews and requests changes
- manager approves for client review
- client approves
- content is marked ready to publish
### 3. Content Package Review
Approval should not be limited to a single file. A review item may include:
- video
- image
- document
- publication message / caption / copy
- hashtags
- links
- publication dates
- target channels or social networks
## Current Workflow Summary
Typical current flow:
1. Team creates media assets.
2. Files are placed in Google Drive by the team or by external providers.
3. A manager sends links by email or message to providers, internal stakeholders, or clients.
4. Feedback comes back by phone, email, spreadsheet, or chat.
5. Team manually consolidates comments across provider feedback and client feedback.
6. A revised version is uploaded.
7. The cycle repeats until someone says it is approved.
8. Approval status is manually tracked elsewhere.
Main failure points:
- no single source of truth
- no structured approval states
- no centralized threaded comments
- no deadline reminders
- no reliable audit trail
- no approval gate before publishing
## Target Workflow
1. Create a project and associate it with a client.
2. Create a review item or approval request.
3. Attach assets or import them from Google Drive.
4. Add metadata:
- title
- publication message / caption / copy
- target platform or social network
- publication dates by network when relevant
- due date
- reviewer(s)
5. Send review request.
6. Reviewers comment directly on the item.
7. Team or provider uploads a revision or responds to comments.
8. System tracks versions, status changes, and workflow events.
9. Reviewer approves, rejects, or requests changes.
10. Once all required approvals are complete, item becomes ready for scheduling/publishing.
## Core Domain Objects
- Workspace: the top-level account boundary for one agency or one operating team.
- Client: the business, creator, or brand receiving the service and approving content.
- Team member: an internal user working on content, reviews, or coordination.
- Reviewer: any person asked to review and approve, whether internal or external.
- Provider: an external production contributor such as a photographer, videographer, editor, or designer.
- Project: the main work container for a client, grouping related content items, notes, participants, and timelines.
- Content item: the reviewable unit that contains assets, publication message, channel targets, due dates, and approval state.
- Asset: an attached file, such as a video, image, or document, referenced from Google Drive or stored directly.
- Asset version: a specific revision of an asset, with traceability to who uploaded it and when.
- Comment thread: a contextual discussion attached to a content item, asset, or revision.
- Approval request: the act of asking one or more reviewers to review a specific version.
- Approval decision: the outcome of a review request, such as approved, rejected, or changes requested.
- Status history: the audit trail of workflow states and transitions over time.
- Publication target: the intended destination for publication, such as Instagram, Facebook, LinkedIn, or a newsletter.
- Notification event: a workflow event that informs users something changed, such as a new comment, revision, request, or approval.
## Suggested Status Model
- Draft
- In internal review
- Changes requested internally
- Internal changes in progress
- Ready for client review
- In client review
- Changes requested by client
- Client changes in progress
- Approved
- Rejected
- Ready to publish
- Published
- Archived
## Minimum Viable Product (MVP) Scope
The first version should focus on approval workflow, not direct publishing.
### MVP Features
- authentication and user roles
- workspace/client/project structure
- create a content item with metadata
- upload assets or attach Google Drive links while keeping Google Drive as the source of truth when required by the client
- version tracking for files and copy
- centralized comments
- approval decisions: approve, reject, request changes
- activity timeline / audit trail
- status dashboard by client, project, and due date
- notifications and reminders when actions are completed or workflow events occur
- simple approval portal for external clients
### Strong MVP Candidate Features
- required approvers
- approval deadline
- due dates per publication target or social network
- compare current version vs previous version
- "latest approved version" indicator
- comment resolution
- filtering by status, client, assignee, due date
## Phase 2 Opportunities
- Google Drive integration with file sync/import
- HootSuite / Metricool export or handoff
- Canva asset linking
- MailChimp approval workflow for newsletters
- calendar integration for publication planning visibility
- annotated comments on images or video timestamps
- reusable approval templates by content type
- Service Level Agreement (SLA) reminders and escalations
- analytics on turnaround time and bottlenecks
- approval by email link
- multi-stage approval rules per client
## Key Automation Opportunities
- auto-request approval when a content item reaches a defined stage
- automatic notifications when a workflow action is completed or a workflow event occurs
- automatic reminders before approval deadlines
- automatic escalation when approval is overdue
- automatic version labeling
- automatic "ready to publish" state when all approvals are complete
- automatic audit trail for every upload, comment, and decision
- automatic client-facing review link generation
- automatic notification when a new revision addresses requested changes
## Important Product Decisions
### 1. System of record for assets
Options:
- keep Google Drive as file storage and build workflow around it
- upload files directly into this new application
- support both
Recommended first assumption:
Keep Google Drive as the source of truth when the client requires ownership there, and support direct uploads later as an option. The first version should work cleanly with Drive links and imported metadata before deeper synchronization is considered.
### 2. External reviewer experience
Options:
- reviewer account required
- magic-link access without full account
- both
Recommended first assumption:
Use magic-link review access for clients to reduce friction.
### 3. Approval granularity
Possible approval units:
- entire content item
- per asset
- per caption/copy
- per channel variation
Recommended first assumption:
Approve at the content item level in the Minimum Viable Product (MVP), with comments attached to assets and copy.
## Business Rules To Confirm
These do not block initial scoping, but we should capture them early so the product behavior matches the real approval process.
- Can a client approve with unresolved comments?
- Does approval require one reviewer or multiple reviewers?
- Can internal approval and client approval happen in parallel?
- Is approval valid only for the latest version?
- Can an approved item be edited without reopening review?
- Do different clients require different workflows?
- Are videos, images, and documents all equally important on day one?
- Is scheduling/publishing inside scope, or only "approval-ready" handoff?
## Open Questions For Next Interview
- Who is the buyer: agency, freelancer, or in-house marketing team?
- Is the first target market agency-to-client approval, internal team approval, or both?
- What content types are highest priority: video, image, documents, captions, newsletters?
- How often do clients request changes after verbal approval?
- What is the most painful step today?
- What tools must remain in place at launch?
- What approvals need legal or compliance traceability?
- How many reviewers usually participate per item?
- Is bilingual support required?
- Is mobile review important on day one?
## Minimum Viable Product (MVP) Success Criteria
- reduce approval turnaround time
- reduce back-and-forth across email/phone/spreadsheets
- give one clear source of truth for latest version and current status
- let a client approve without training
- let the team see blocked items instantly
## Product Positioning
This product should be positioned as:
"A review and approval workflow for social media content, not another content creation tool."
The value is coordination, traceability, and faster approval cycles.
## First Build Recommendation
Build the first release around this narrow flow:
1. team creates content item
2. team uploads files and copy
3. internal reviewer comments and requests changes
4. team submits to client
5. client comments and approves via simple link
6. item becomes ready for publishing handoff
If this flow works cleanly, integrations and scheduling can be added later.

View File

@@ -1,32 +0,0 @@
# Stripe
> Legacy Hutopy-era notes. These flows describe the old creator membership and tipping model and do not match the current workflow product.
## Events Workflow
### Membership
1. checkout.session.completed
- Store StripeSubscriptionId, UserId, CreatorId, TierId
- Save a new Subscription entity with the status "Pending"
2. invoice.payment_succeeded
- Grant access (set Subscription.Active = true or similar)
- Record transaction or set StartDate
- Notify Creator (e.g., new member)
3. customer.subscription.updated
- Update `EndDate = CancelAt ?? CanceledAt`
4. customer.subscription.deleted
- Revoke access
- Mark Subscription as inactive/ended
### Tips
1. checkout.session.completed
- Store TipId, StripeSessionId, TipperId, CreatorId
- PaymentIntentStatus == "paid"
- Status = "Paid"
- Notify creator
- Record transaction

View File

@@ -1,35 +0,0 @@
> Historical worksheet from the product-pivot phase. Several items are completed, renamed, or superseded by the current codebase.
What is left to do:
1. Replace frontend seeded stores with real API-backed stores.
2. Build authenticated loading for:
- workspaces
- clients
- projects
- content items
3. Add create flows:
- create workspace
- create client
- create project
- create content item
4. Build the first real content-item detail page.
5. Add the actual approval workflow domain:
- approval requests
- approval decisions
- status transitions
6. Add comments and revision tracking.
7. Add Google Drive asset linkage instead of placeholder source fields only.
8. Add notification/event backbone.
9. Remove or retire old Hutopy modules and routes once the new vertical slice replaces them.
10. Rename remaining Hutopy namespaces/product strings in the backend if you want the codebase semantics
cleaned up now instead of later.
So the main remaining substantive work is:
1. finish full-site localization
2. build real integrations backend for Google Drive and API keys
3. deepen workspace/settings management
4. continue workflow polish and broader comment/thread modeling if you want that
generalized again

View File

@@ -45,5 +45,4 @@ These are cross-cutting rules for the current product and codebase. They are int
## Naming Constraints
- Prefer current product/domain language over Hutopy-era terminology.
- Avoid reviving creator/tipping/membership concepts unless intentionally rebuilding them for the new product.

View File

@@ -1,4 +1,4 @@
VITE_API_URL=/api
VITE_API_URL=/
VITE_STRIPE_API_KEY=51OoveVDrRyqXtNdBAxIo183PujtqFyU0xUMK9YNtIijcHeDlcLN6pqkZWHbgaBA0FHrwLMSoy3yVLN33NX8ExOxL00MSZwgJN7
VITE_GOOGLE_CLIENT_ID=213344094492-7c83lqoh7mnjgadpeqo2lcs1krhbsnnd.apps.googleusercontent.com
VITE_FACEBOOK_APP_ID=1076433907621883

View File

@@ -1,7 +1,7 @@
FROM node:22-alpine AS build
WORKDIR /app
ARG VITE_API_URL=/api
ARG VITE_API_URL=/
ENV VITE_API_URL=$VITE_API_URL
COPY frontend/package*.json ./
@@ -12,3 +12,4 @@ RUN npm run build
FROM caddy:2-alpine AS runtime
COPY --from=build /app/dist /srv
COPY deploy/caddy/Caddyfile /etc/caddy/Caddyfile

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 240" role="img" aria-labelledby="title">
<title id="title">Socialize brand illustration</title>
<defs>
<linearGradient id="brand-gradient" x1="102" y1="52" x2="324" y2="194" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ff8a3d"/>
<stop offset="1" stop-color="#ef4444"/>
</linearGradient>
</defs>
<rect width="420" height="240" rx="36" fill="#fbfaf6"/>
<rect x="58" y="44" width="304" height="152" rx="30" fill="#f4f6f3" stroke="#c7d2cc"/>
<rect x="82" y="68" width="112" height="104" rx="28" fill="url(#brand-gradient)"/>
<path d="M113 137c5.7 5.8 13.2 8.8 22.5 8.8 10.4 0 16.3-3.4 16.3-9.4 0-5.2-4.4-7.4-16-9.1-15.2-2.3-24.2-7.8-24.2-20.2 0-12.1 10.4-20.3 25.5-20.3 10.9 0 19.2 3.3 25.4 10.1l-9.6 8.9c-4.2-4.3-9.4-6.5-15.7-6.5-6.9 0-10.7 2.7-10.7 7.2 0 4.4 4.4 6.4 15.2 8 15.9 2.4 24.6 8.5 24.6 21.1 0 13.6-11 22-29.4 22-12.8 0-23-4-30.2-12z" fill="#fffaf2"/>
<path d="M224 86h72" stroke="#172033" stroke-width="12" stroke-linecap="round"/>
<path d="M224 118h112" stroke="#2fa58d" stroke-width="12" stroke-linecap="round"/>
<path d="M224 150h84" stroke="#ff8a3d" stroke-width="12" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-labelledby="title">
<title id="title">Socialize mark</title>
<defs>
<linearGradient id="brand-gradient" x1="16" y1="12" x2="82" y2="88" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ff8a3d"/>
<stop offset="1" stop-color="#ef4444"/>
</linearGradient>
</defs>
<rect width="96" height="96" rx="28" fill="url(#brand-gradient)"/>
<path d="M31 61.5c3.9 4.3 9.3 6.5 16.2 6.5 7.8 0 12.4-2.7 12.4-7.3 0-4.1-3.4-5.9-12.2-7.2-12.1-1.8-19-6.2-19-16.1 0-9.7 8.2-16.4 20.2-16.4 8.7 0 15.3 2.7 20.3 8.2l-8.3 8c-3.3-3.5-7.5-5.2-12.5-5.2-5.4 0-8.3 2.1-8.3 5.6 0 3.6 3.4 5.1 11.9 6.4 12.7 1.9 19.5 6.8 19.5 16.8 0 11-8.8 17.8-23.4 17.8-10.2 0-18.3-3.2-24.2-9.7z" fill="#fffaf2"/>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 96" role="img" aria-labelledby="title">
<title id="title">Socialize</title>
<defs>
<linearGradient id="brand-gradient" x1="16" y1="12" x2="82" y2="88" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ff8a3d"/>
<stop offset="1" stop-color="#ef4444"/>
</linearGradient>
</defs>
<rect width="96" height="96" rx="28" fill="url(#brand-gradient)"/>
<path d="M31 61.5c3.9 4.3 9.3 6.5 16.2 6.5 7.8 0 12.4-2.7 12.4-7.3 0-4.1-3.4-5.9-12.2-7.2-12.1-1.8-19-6.2-19-16.1 0-9.7 8.2-16.4 20.2-16.4 8.7 0 15.3 2.7 20.3 8.2l-8.3 8c-3.3-3.5-7.5-5.2-12.5-5.2-5.4 0-8.3 2.1-8.3 5.6 0 3.6 3.4 5.1 11.9 6.4 12.7 1.9 19.5 6.8 19.5 16.8 0 11-8.8 17.8-23.4 17.8-10.2 0-18.3-3.2-24.2-9.7z" fill="#fffaf2"/>
<text x="126" y="61" fill="#172033" font-family="Inter, Arial, sans-serif" font-size="35" font-weight="900" letter-spacing="6">SOCIALIZE</text>
</svg>

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 741 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 791 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1202 376" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M68,96C68.09,93.398 71.421,91.358 74,91C80,90.167 98,90.167 104,91C106.579,91.358 109.774,93.406 110,96C111,107.5 110,160 110,160L171,160C171,160 170.167,107.5 171,96C171.17,93.649 173.665,91.324 176,91C182,90.167 200.833,90.5 207,91C209.229,91.181 212.922,91.765 213,94C214,122.667 214,234 213,263C212.91,265.602 209.58,267.651 207,268C200.833,268.833 182,268.333 176,268C174.208,267.9 171.13,267.79 171,266C170.167,254.5 171,199 171,199L110,200C110,200 110.833,251.667 110,263C109.827,265.351 107.336,267.684 105,268C98.833,268.833 79.167,268.667 73,268C70.878,267.771 68.062,266.133 68,264C67.167,235.333 67,124.833 68,96Z" style="fill:white;"/>
<path d="M251,96C251.374,93.461 252.451,91.071 256,91C261.475,90.891 285.614,90.963 289,91C291.413,91.026 292.894,92.908 293,96C293.106,99.092 292.667,193 294,215C294.298,219.913 297.667,224.833 301,228C304.333,231.167 309.314,233.093 314,234C319.167,235 326.5,235.667 332,234C337.5,232.333 343.833,228 347,224C350.013,220.195 350.848,214.851 351,210C351.667,188.667 350.826,101.008 351,96C351.112,92.772 355.006,90.989 357,91C360.962,91.021 387.092,91.088 389,91C390.908,90.912 393.622,92.504 394,94C394.611,96.417 394.589,216.837 394,222C393.746,224.227 391.394,231.223 390,234C388.606,236.777 386.179,243.243 383,247C379.333,251.333 373.699,256.639 368,260C361.5,263.833 352.525,268.437 344,270C334,271.833 318.833,272.333 308,271C297.954,269.764 286.5,265.667 279,262C272.826,258.982 267.333,254.333 263,249C258.667,243.667 254.05,235.036 253,230C251.95,224.964 250.95,222.146 251,219C251.05,215.854 250.579,98.856 251,96Z" style="fill:white;"/>
<path d="M419,94C419,94 421.048,91.261 423,91C426.899,90.478 556,91 556,91C556,91 558.986,93.053 559,95C559.017,97.459 559,125 559,125C559,125 555.96,129.013 553,129C548.051,128.979 510,129 510,129L510,264C510,264 507.857,267.915 505,268C491.15,268.413 474.868,268.106 473,268C471.132,267.894 468.048,265.463 468,263C467.917,258.698 468,129 468,129L425,129C425,129 419.265,126.448 419,124C418.735,121.552 419,94 419,94Z" style="fill:white;"/>
<path d="M1053,157C1053,157 1023.67,104 1017,93C1016.23,91.725 1014.79,91.066 1013,91C1004,90.667 982.833,91.167 976,91C973.866,90.948 971.045,94.091 972,96C981.167,114.333 1031,201 1031,201C1031,201 1030.5,250.833 1031,262C1031.1,264.234 1031.76,268 1034,268C1040.33,268 1062.5,269 1069,268C1071.38,267.635 1072.82,264.397 1073,262C1073.83,250.833 1074,201 1074,201C1074,201 1120.17,116.333 1129,98C1130.05,95.814 1128.67,91 1127,91C1119.67,91 1096.67,90.667 1090,91C1088.8,91.06 1087.59,91.952 1087,93C1080.83,104 1053,157 1053,157Z" style="fill:white;"/>
<path d="M583,129C583,129 595.383,116.769 603,112C610.617,107.231 623.098,101.982 634,100C645,98 658.167,98.167 669,100C679.502,101.777 691.333,107 699,111C705.092,114.179 710.333,119.333 715,124C719.528,128.528 723.5,133.833 727,139C730.432,144.066 733.922,149.245 736,155C738.167,161 739.411,168.227 740,175C740.667,182.667 741,193.167 740,201C739.078,208.222 736.957,215.347 734,222C730.667,229.5 724.667,239.833 720,246C716.157,251.078 711.211,255.339 706,259C699.833,263.333 691,269 683,272C675.043,274.984 666,276.333 658,277C650.353,277.637 642.167,277.167 635,276C628.13,274.882 622.163,273.477 615,270C607.837,266.523 604.664,264.241 601,262C597.336,259.759 605.84,268.443 612,273C618.16,277.557 624.166,281.932 631,285C639.167,288.667 650.976,293.601 661,295C675.333,297 697.167,295.833 712,292C725.859,288.419 738.82,280.939 750,272C758.702,265.043 764.288,259.006 770.928,250.283C776.178,243.388 780.836,235.064 784,228C791.167,212 795.54,193.407 793,176C788.5,145.167 783.045,134.058 769,115C759.281,101.813 743,90.167 729,83C715.543,76.111 699.667,72.667 685,72C670.333,71.333 654,74.5 641,79C628.575,83.301 614.357,92.576 607,99C594.828,109.629 583,129 583,129Z" style="fill:rgb(107,0,101);"/>
<path d="M593,148C593,147.167 597.287,141.032 600,138C602.833,134.833 606.667,131.667 610,129C613.177,126.458 616.667,123.833 620,122C623.146,120.27 626.579,119.089 630,118C633.667,116.833 637.9,115.439 642,115C646.667,114.5 653.833,114.833 658,115C661.016,115.121 664.079,115.238 667,116C670.833,117 677,119.167 681,121C684.534,122.62 687.908,124.644 691,127C694.5,129.667 698.667,133.333 702,137C705.333,140.667 708.519,144.659 711,149C713.667,153.667 716.5,160.5 718,165C719.179,168.536 719.744,172.282 720,176C720.333,180.833 720.833,188.333 720,194C719.187,199.528 716.667,205.667 715,210C713.662,213.478 711.99,216.849 710,220C708,223.167 705.5,226.333 703,229C700.577,231.585 697.781,233.804 695,236C691.833,238.5 688.055,241.972 684,244C679.333,246.333 672.333,248.667 667,250C662.106,251.223 657.042,252.144 652,252C646.167,251.833 637.333,250.333 632,249C627.796,247.949 623.333,245.5 620,244C617.281,242.777 614.417,241.746 612,240C609,237.833 601.833,231.667 602,231C602.167,230.333 609.833,234.667 613,236C615.625,237.105 618.228,238.348 621,239C623.833,239.667 626.985,239.856 630,240C633.5,240.167 636.325,240.389 642,240C646.467,239.694 650.667,238.167 654,237C656.814,236.015 659.472,234.58 662,233C664.667,231.333 667.563,229.274 670,227C672.5,224.667 674.898,221.853 677,219C679.333,215.833 682.189,211.951 684,208C685.833,204 686.333,204.167 688,195C688.718,191.051 689.308,187.002 689,183C688.667,178.667 687.167,173 686,169C684.995,165.553 683.73,162.146 682,159C680.167,155.667 677.667,152 675,149C672.333,146 668.833,143.167 666,141C663.502,139.09 660.868,137.29 658,136C654.667,134.5 649.333,132.833 646,132C643.393,131.348 640.683,131.158 638,131C635.167,130.833 634.5,130.667 629,131C626.256,131.166 622.635,131.557 618,133C616.028,133.614 611,135.333 608,137C605.086,138.619 602.5,141.167 600,143C597.688,144.696 593,148.833 593,148Z" style="fill:rgb(163,14,121);"/>
<path d="M823,91C822.011,92.019 820.034,94.057 820,96C819.5,124.667 819.333,234.333 820,263C820.05,265.134 822.056,268.008 824,268C833,267.961 837.833,268.021 857,268C858.886,267.998 861.779,265.347 862,263C862.833,254.167 862,215 862,215C862,215 888.5,215.667 898,215C905.108,214.501 912.308,213.448 919,211C925.833,208.5 933.667,204.167 939,200C943.843,196.216 948.002,191.365 951,186C954.167,180.333 956.667,171.833 958,166C959.117,161.115 959,155.667 959,151C959,146.654 958.911,142.25 958,138C957,133.333 955.489,127.646 953,123C950.5,118.333 946.833,113.833 943,110C939.167,106.167 934.667,102.667 930,100C925.333,97.333 920.195,95.417 915,94C909.5,92.5 903.08,91.198 897,91C881.667,90.5 842.556,90.833 823,91M863,129L863,179C863,179 889,178.5 896,178C899.065,177.781 902.314,177.492 905,176C908,174.333 911.887,171.413 914,168C916.167,164.5 917.667,159.333 918,155C918.333,150.667 917.5,145.5 916,142C914.604,138.743 911.833,136 909,134C906.167,132 902.569,130.388 899,130C891.333,129.167 863,129 863,129Z" style="fill:white;"/>
</svg>

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

View File

@@ -1,5 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="540" height="540 ">
<path fill="currentColor" d="M0 0 C2.69849689 0.0031451 5.39587704 -0.02033719 8.09423828 -0.0456543 C24.64095924 -0.10755047 40.6877731 1.83145667 56.59814453 6.50317383 C57.64365479 6.80505615 58.68916504 7.10693848 59.76635742 7.41796875 C92.79656028 17.21576522 120.14877162 34.84217931 145.09814453 58.31567383 C145.80712891 58.97051758 146.51611328 59.62536133 147.24658203 60.30004883 C156.43027452 69.07141226 164.10800524 79.75645166 171.09814453 90.31567383 C171.99726562 91.64985352 171.99726562 91.64985352 172.91455078 93.01098633 C202.24624509 138.40104257 210.21713133 193.46657021 199.45556641 245.92919922 C192.90962056 275.68852135 179.08829353 305.20779517 159.09814453 328.31567383 C158.12705075 329.46844293 157.15708766 330.62216508 156.18798828 331.77661133 C145.74534244 344.15953661 145.74534244 344.15953661 140.09814453 349.31567383 C139.43814453 349.31567383 138.77814453 349.31567383 138.09814453 349.31567383 C138.09814453 349.97567383 138.09814453 350.63567383 138.09814453 351.31567383 C125.46428062 363.48565937 109.61785837 373.30307857 94.09814453 381.31567383 C93.49325195 381.62843262 92.88835937 381.94119141 92.26513672 382.26342773 C64.72703152 396.27155233 34.49385055 402.76232703 3.72314453 402.69067383 C2.84274506 402.69013 1.96234558 402.68958618 1.05526733 402.68902588 C-13.25349056 402.64848426 -26.95433873 401.73425935 -40.90185547 398.31567383 C-42.23821045 397.99421387 -42.23821045 397.99421387 -43.6015625 397.66625977 C-72.07884569 390.61747809 -98.87807183 378.00655878 -121.53857422 359.26879883 C-124.98230298 356.422742 -128.53024689 353.7103623 -132.06591797 350.97973633 C-132.97470703 350.15602539 -132.97470703 350.15602539 -133.90185547 349.31567383 C-133.90185547 348.65567383 -133.90185547 347.99567383 -133.90185547 347.31567383 C-134.56185547 347.31567383 -135.22185547 347.31567383 -135.90185547 347.31567383 C-135.90185547 346.65567383 -135.90185547 345.99567383 -135.90185547 345.31567383 C-136.56185547 345.31567383 -137.22185547 345.31567383 -137.90185547 345.31567383 C-139.24169922 343.98754883 -139.24169922 343.98754883 -140.83935547 342.06567383 C-143.10188476 339.38611053 -145.39815773 336.76489417 -147.80810547 334.21801758 C-148.30834229 333.68660156 -148.8085791 333.15518555 -149.32397461 332.60766602 C-150.32555716 331.54718583 -151.33248849 330.49172924 -152.3449707 329.44165039 C-155.90185547 325.64913211 -155.90185547 325.64913211 -155.90185547 322.31567383 C-151.7526342 322.86293323 -149.09751951 325.32327358 -145.96435547 327.87817383 C-112.49996321 354.28872765 -70.28107399 364.92725762 -28.16088867 360.37231445 C11.7404784 355.37903429 50.51367105 333.83116901 75.28564453 302.00317383 C102.2073581 266.1942078 113.74287629 223.77918143 108.03564453 179.19067383 C103.36182966 146.79964046 88.57557441 116.98148746 66.09814453 93.31567383 C65.60604492 92.78570801 65.11394531 92.25574219 64.60693359 91.7097168 C40.8738918 66.25446714 8.58347239 50.05740641 -25.90185547 45.31567383 C-27.07103516 45.13391602 -28.24021484 44.9521582 -29.44482422 44.76489258 C-71.61533582 39.65438979 -112.89208728 51.7135382 -146.31201172 77.46020508 C-149.8276055 80.21952147 -153.19809546 83.10839876 -156.42138672 86.20629883 C-157.90185547 87.31567383 -157.90185547 87.31567383 -160.90185547 87.31567383 C-160.35570146 83.08040407 -157.80081795 80.22554824 -155.21435547 77.00317383 C-154.72813721 76.3936084 -154.24191895 75.78404297 -153.7409668 75.15600586 C-143.89916607 62.99985508 -133.13277345 52.06245136 -120.90185547 42.31567383 C-120.33450684 41.85741211 -119.7671582 41.39915039 -119.18261719 40.92700195 C-97.55281967 23.63297615 -71.10136924 11.56026197 -44.27685547 5.06567383 C-43.26478027 4.81954346 -42.25270508 4.57341309 -41.20996094 4.31982422 C-27.42871573 1.10789603 -14.10433559 -0.03420308 0 0 Z " transform="translate(258.90185546875,69.684326171875)"/>
<path fill="currentColor" d="M0 0 C23.93418963 20.21356155 38.18828481 47.87411238 43.30859375 78.609375 C43.72796962 83.82104607 43.79621346 89.00908218 43.74609375 94.234375 C43.7423877 94.93147583 43.73868164 95.62857666 43.73486328 96.34680176 C43.51079954 126.08447361 33.84943539 152.98108038 14.30859375 175.609375 C13.37273437 176.74439453 13.37273437 176.74439453 12.41796875 177.90234375 C7.3119221 183.93204517 1.58692987 188.83219983 -4.69140625 193.609375 C-5.65691406 194.351875 -6.62242187 195.094375 -7.6171875 195.859375 C-33.76277727 214.99515608 -66.44544142 222.27976227 -98.37890625 217.421875 C-129.2224086 212.211016 -156.10496998 196.18588064 -175.99609375 172.10546875 C-177.69140625 169.609375 -177.69140625 169.609375 -177.69140625 166.609375 C-176.37140625 166.939375 -175.05140625 167.269375 -173.69140625 167.609375 C-173.69140625 168.269375 -173.69140625 168.929375 -173.69140625 169.609375 C-172.85222656 170.00576172 -172.85222656 170.00576172 -171.99609375 170.41015625 C-169.47801141 171.72041265 -167.18096648 173.22649078 -164.81640625 174.796875 C-141.95286732 189.35191305 -116.13355444 194.94004479 -89.48388672 189.12792969 C-74.00965166 185.43003002 -61.09388018 178.62819028 -48.69140625 168.609375 C-48.05976563 168.14273438 -47.428125 167.67609375 -46.77734375 167.1953125 C-29.95140734 154.19707727 -19.18769244 130.97695501 -16.03515625 110.39453125 C-15.865 109.01587891 -15.865 109.01587891 -15.69140625 107.609375 C-15.55734375 106.55621094 -15.42328125 105.50304687 -15.28515625 104.41796875 C-12.8813248 80.40697048 -19.85309354 55.11934523 -34.69921875 36.02734375 C-35.35664063 35.22941406 -36.0140625 34.43148437 -36.69140625 33.609375 C-37.32820312 32.82175781 -37.965 32.03414062 -38.62109375 31.22265625 C-54.38763523 12.57508008 -77.98681734 0.98006353 -102.19970703 -1.04858398 C-115.33840527 -1.9297872 -128.14870541 -0.56547477 -140.69140625 3.609375 C-141.71621094 3.94710937 -142.74101562 4.28484375 -143.796875 4.6328125 C-155.05737548 8.64192736 -164.81345042 14.73678833 -173.984375 22.359375 C-175.69140625 23.609375 -175.69140625 23.609375 -177.69140625 23.609375 C-176.02661937 12.90007435 -162.17010016 2.50403181 -153.91601562 -3.73193359 C-107.16913616 -37.54750108 -44.81974776 -36.84949238 0 0 Z " transform="translate(291.69140625,175.390625)"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

View File

@@ -27,7 +27,7 @@ const disallowedRoutes = [
'/verify-email',
];
const siteUrl = (process.env.VITE_PUBLIC_SITE_URL ?? process.env.SITE_URL ?? 'https://socialize.mapachotes.com')
const siteUrl = (process.env.VITE_PUBLIC_SITE_URL ?? process.env.SITE_URL ?? 'http://localhost:5173')
.replace(/\/$/, '');
const distDir = resolve(process.cwd(), 'dist');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,63 @@
import { branding } from './branding.js';
const cssVariableMap = {
'--socialize-primary': 'primary',
'--socialize-accent': 'accent',
'--socialize-accent-strong': 'accentStrong',
'--socialize-highlight': 'highlight',
'--h-background': 'background',
'--h-on-background': 'onBackground',
'--h-surface': 'surface',
'--h-surface-muted': 'surfaceMuted',
'--h-on-surface': 'onSurface',
'--h-control': 'control',
'--h-control-hover': 'controlHover',
'--h-control-focus': 'controlFocus',
'--h-border': 'border',
'--h-border-strong': 'borderStrong',
'--h-primary': 'primary',
'--h-on-primary': 'onPrimary',
'--h-secondary': 'secondary',
'--h-on-secondary': 'onSecondary',
'--h-tertiary': 'tertiary',
'--h-on-tertiary': 'onTertiary',
'--h-error': 'error',
'--h-on-error': 'onError',
};
export function applyBranding(target = getDefaultTarget()) {
if (!target) {
return;
}
Object.entries(cssVariableMap).forEach(([variableName, colorKey]) => {
target.style.setProperty(variableName, branding.colors[colorKey]);
});
target.style.setProperty(
'--socialize-brand-gradient',
`linear-gradient(135deg, ${branding.colors.accent} 0%, ${branding.colors.accentStrong} 100%)`
);
target.style.setProperty('--socialize-accent-shadow', getRgbShadow(branding.colors.accent, 0.28));
target.style.setProperty('--socialize-accent-strong-shadow', getRgbShadow(branding.colors.accentStrong, 0.28));
}
function getDefaultTarget() {
return typeof document === 'undefined'
? null
: document.documentElement;
}
function getRgbShadow(hexColor, opacity) {
const normalizedHex = hexColor.replace('#', '');
if (normalizedHex.length !== 6) {
return hexColor;
}
const red = Number.parseInt(normalizedHex.slice(0, 2), 16);
const green = Number.parseInt(normalizedHex.slice(2, 4), 16);
const blue = Number.parseInt(normalizedHex.slice(4, 6), 16);
return `rgba(${red}, ${green}, ${blue}, ${opacity})`;
}

View File

@@ -0,0 +1,50 @@
export const branding = Object.freeze({
productName: 'Socialize',
shortName: 'S',
assets: {
logo: '/images/brand/logo.svg',
logoMark: '/images/brand/logo-mark.svg',
authIllustration: '/images/brand/auth-illustration.svg',
favicon: '/favicon.ico',
},
colors: {
background: '#f4f6f3',
onBackground: '#172033',
surface: '#fbfaf6',
surfaceMuted: '#f1f5f2',
onSurface: '#172033',
control: '#eef3ef',
controlHover: '#e7eee9',
controlFocus: '#ffffff',
border: '#c7d2cc',
borderStrong: '#94a39d',
primary: '#172033',
onPrimary: '#fbfaf6',
secondary: '#fff3e2',
onSecondary: '#172033',
tertiary: '#d9f6ee',
onTertiary: '#0f766e',
accent: '#ff8a3d',
accentStrong: '#ef4444',
highlight: '#2fa58d',
error: '#bc2f2f',
onError: '#ffffff',
info: '#2563eb',
success: '#2fa58d',
warning: '#b45309',
},
});
export function getVuetifyThemeColors() {
return {
background: branding.colors.background,
surface: branding.colors.surface,
primary: branding.colors.primary,
secondary: branding.colors.secondary,
accent: branding.colors.accent,
error: branding.colors.error,
info: branding.colors.info,
success: branding.colors.success,
warning: branding.colors.warning,
};
}

View File

@@ -0,0 +1,35 @@
<template>
<span class="brand-logo-root">
<img
v-if="branding.assets.logo"
:src="branding.assets.logo"
:alt="branding.productName"
class="brand-logo-image"
/>
<span
v-else
class="brand-logo-text"
>
{{ branding.productName }}
</span>
</span>
</template>
<script setup>
import { branding } from '@/branding/branding.js';
</script>
<style scoped>
.brand-logo-root {
@apply inline-flex items-center;
}
.brand-logo-image {
@apply block h-auto max-h-full w-auto max-w-full;
}
.brand-logo-text {
@apply text-lg font-black uppercase tracking-[0.18em];
color: var(--h-primary);
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<span class="brand-mark-root">
<img
v-if="branding.assets.logoMark"
:src="branding.assets.logoMark"
alt=""
aria-hidden="true"
class="brand-mark-image"
/>
<span v-else>{{ branding.shortName }}</span>
</span>
</template>
<script setup>
import { branding } from '@/branding/branding.js';
</script>
<style scoped>
.brand-mark-root {
@apply inline-flex items-center justify-center overflow-hidden;
}
.brand-mark-image {
@apply h-full w-full object-contain;
}
</style>

View File

@@ -5,8 +5,7 @@
class="login-brand"
to="/"
>
<span class="login-brand-mark">S</span>
<span class="login-brand-text">Socialize</span>
<BrandLogo class="login-brand-logo" />
</router-link>
<section class="login-card">
@@ -131,6 +130,7 @@
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { mdiEye, mdiEyeOff, mdiFacebook, mdiGoogle } from '@mdi/js';
import BrandLogo from '@/components/branding/BrandLogo.vue';
const { t } = useI18n();
const router = useRouter();
@@ -198,7 +198,10 @@
}
function resendVerification() {
router.push('/verify-email');
router.push({
path: '/verify-email',
query: email.value ? { email: email.value.trim() } : {},
});
}
</script>
@@ -212,18 +215,11 @@
}
.login-brand {
@apply mx-auto flex items-center gap-3 px-4 pt-6 no-underline sm:px-0 sm:pt-0;
color: #172033;
@apply mx-auto flex items-center px-4 pt-6 no-underline sm:px-0 sm:pt-0;
}
.login-brand-mark {
@apply flex h-11 w-11 items-center justify-center rounded-2xl text-lg font-black;
background: var(--socialize-brand-gradient);
color: #fffaf2;
}
.login-brand-text {
@apply text-lg font-black uppercase tracking-[0.18em];
.login-brand-logo {
@apply h-12 max-w-[180px];
}
.login-card {

View File

@@ -7,7 +7,8 @@
>
<img
:alt="t('alt')"
src="/images/hutopymedia/loginpage/hutopylogin.svg"
:src="branding.assets.authIllustration"
class="auth-illustration"
/>
<div class="flex flex-col gap-10 text-center">
<h1 class="login-text text-2xl font-bold text-green-600">
@@ -41,7 +42,8 @@
>
<img
:alt="t('alt')"
src="/images/hutopymedia/loginpage/hutopylogin.svg"
:src="branding.assets.authIllustration"
class="auth-illustration"
/>
<div class="flex flex-col gap-10">
<h1 class="login-text text-center text-2xl font-bold">
@@ -133,9 +135,12 @@
import { ref } from 'vue';
import { useClient } from '@/plugins/api.js';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { mdiEye, mdiEyeOff } from '@mdi/js';
import { branding } from '@/branding/branding.js';
const { t } = useI18n();
const router = useRouter();
const clientApi = useClient();
const name = ref('');
@@ -165,9 +170,12 @@
password: password.value,
});
// On success, show verification message
userEmail.value = email.value.trim();
registrationSuccess.value = true;
await router.push({
path: '/verify-email',
query: { email: userEmail.value, pending: '1' },
});
} catch (error) {
console.error('Registration failed:', error);
errorMessage.value = error.response?.data?.message || t('registrationFailed');
@@ -185,6 +193,10 @@
@apply z-10;
}
.auth-illustration {
@apply h-auto w-full max-w-xs;
}
/* Override Vuetify's default padding to accommodate our icon */
:deep(.v-field__append-inner) {
padding-inline-start: 0;
@@ -195,7 +207,7 @@
{
"en": {
"title": "Create your account",
"alt": "Hutopy Registration",
"alt": "Socialize registration",
"name": "Full Name",
"email": "Email",
"password": "Password",
@@ -215,7 +227,7 @@
},
"fr": {
"title": "Créer votre compte",
"alt": "Inscription Hutopy",
"alt": "Inscription Socialize",
"name": "Nom complet",
"email": "Email",
"password": "Mot de passe",

View File

@@ -36,28 +36,18 @@
<!-- Error state -->
<div
v-else
v-else-if="showResendOnly"
class="flex flex-col items-center gap-6"
>
<v-icon
color="error"
icon="mdi-alert-circle"
color="primary"
icon="mdi-email-sync"
size="64"
></v-icon>
<h1 class="text-2xl font-bold text-red-600">{{ t('error.title') }}</h1>
<p>{{ errorMessage || t('error.defaultMessage') }}</p>
<h1 class="text-2xl font-bold">{{ t('pending.title') }}</h1>
<p>{{ t('pending.message') }}</p>
<div class="mt-4 flex flex-col gap-4 w-full">
<v-btn
color="primary"
@click="goToLogin"
>
{{ t('error.goToLogin') }}
</v-btn>
<v-divider class="my-4"></v-divider>
<!-- Resend verification email section -->
<h2 class="text-xl font-medium">{{ t('resend.title') }}</h2>
<v-form
class="w-full"
@submit.prevent="handleResendVerification"
@@ -99,6 +89,69 @@
</v-form>
</div>
</div>
<!-- Error state -->
<div
v-else
class="flex flex-col items-center gap-6"
>
<v-icon
color="error"
icon="mdi-alert-circle"
size="64"
></v-icon>
<h1 class="text-2xl font-bold text-red-600">{{ t('error.title') }}</h1>
<p>{{ errorMessage || t('error.defaultMessage') }}</p>
<div class="mt-4 flex flex-col gap-4 w-full">
<v-btn
color="primary"
@click="goToLogin"
>
{{ t('error.goToLogin') }}
</v-btn>
<v-divider class="my-4"></v-divider>
<h2 class="text-xl font-medium">{{ t('resend.title') }}</h2>
<v-form
class="w-full"
@submit.prevent="handleResendVerification"
>
<div class="flex flex-col gap-4">
<v-text-field
v-model="resendEmail"
:error-messages="resendEmailError"
:label="t('resend.emailLabel')"
required
type="email"
></v-text-field>
<v-btn
:loading="resendLoading"
block
color="secondary"
type="submit"
>
{{ t('resend.button') }}
</v-btn>
<div
v-if="resendSuccess"
class="mt-2 p-3 bg-green-50 border border-green-200 rounded text-green-700 text-sm"
>
{{ t('resend.success') }}
</div>
<div
v-if="resendError"
class="mt-2 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm"
>
{{ resendError }}
</div>
</div>
</v-form>
</div>
</div>
</div>
</div>
</template>
@@ -118,6 +171,7 @@
const isLoading = ref(true);
const verificationSuccess = ref(false);
const errorMessage = ref('');
const showResendOnly = ref(false);
// Resend verification state
const resendEmail = ref('');
@@ -138,7 +192,11 @@
// Check if we have the required parameters
if (!userId || !token) {
isLoading.value = false;
errorMessage.value = t('error.missingParams');
if (route.query.email || route.query.pending) {
showResendOnly.value = true;
} else {
errorMessage.value = t('error.missingParams');
}
return;
}
@@ -202,8 +260,13 @@
"missingParams": "Missing required verification parameters.",
"goToLogin": "Go to Login"
},
"pending": {
"title": "Check your email",
"message": "We sent a verification link to your inbox. You can request a new link if it doesn't arrive."
},
"resend": {
"title": "Resend Verification Email",
"description": "Enter your account email and we'll send a new verification link.",
"emailLabel": "Email",
"button": "Resend Verification Email",
"success": "Verification email sent successfully. Please check your inbox.",
@@ -224,8 +287,13 @@
"missingParams": "Paramètres de vérification requis manquants.",
"goToLogin": "Aller à la connexion"
},
"pending": {
"title": "Vérifiez votre email",
"message": "Nous avons envoyé un lien de vérification dans votre boîte de réception. Vous pouvez demander un nouveau lien s'il n'arrive pas."
},
"resend": {
"title": "Renvoyer l'email de vérification",
"description": "Entrez l'email de votre compte et nous enverrons un nouveau lien de vérification.",
"emailLabel": "Email",
"button": "Renvoyer l'email de vérification",
"success": "Email de vérification envoyé avec succès. Veuillez vérifier votre boîte de réception.",

View File

@@ -8,6 +8,8 @@
import { getNotificationRoute } from '@/features/notifications/notificationRoutes.js';
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
import { branding } from '@/branding/branding.js';
import BrandMark from '@/components/branding/BrandMark.vue';
import SidebarUserMenu from './SidebarUserMenu.vue';
import {
mdiBellOutline,
@@ -253,14 +255,14 @@
:class="{ 'brand-link-collapsed': !isExpanded }"
to="/"
>
<span class="brand-mark">S</span>
<BrandMark class="brand-mark" />
<div
v-if="isExpanded"
class="brand-copy"
>
<div class="brand-heading">
<span class="brand-name-wrap">
<span class="brand-name">Socialize</span>
<span class="brand-name">{{ branding.productName }}</span>
<span
class="brand-stage-badge"
:aria-label="t('nav.brandStageLabel')"
@@ -664,9 +666,7 @@
}
.brand-mark {
@apply flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[1.1rem] text-xl font-black;
background: var(--socialize-brand-gradient);
color: #fffaf2;
@apply h-11 w-11 flex-shrink-0 rounded-[1.1rem];
}
.brand-name-wrap {
@@ -675,7 +675,7 @@
.brand-name {
@apply min-w-0 text-lg font-black uppercase tracking-[0.18em];
color: #172033;
color: var(--h-primary);
line-height: 2.75rem;
}

View File

@@ -35,6 +35,10 @@ import { useChannelsStore } from '@/features/channels/stores/channelsStore.js';
import { i18n } from '@/plugins/i18n.js';
import config from '@/config.js';
import { createHead } from '@vueuse/head';
import { applyBranding } from '@/branding/applyBranding.js';
import { getVuetifyThemeColors } from '@/branding/branding.js';
applyBranding();
const vuetify = createVuetify({
components: {
@@ -62,17 +66,7 @@ const vuetify = createVuetify({
themes: {
socializeLight: {
dark: false,
colors: {
background: '#f4f6f3',
surface: '#fbfaf6',
primary: '#172033',
secondary: '#fff3e2',
accent: '#ff8a3d',
error: '#bc2f2f',
info: '#2563eb',
success: '#2fa58d',
warning: '#b45309',
},
colors: getVuetifyThemeColors(),
},
},
},

View File

@@ -5,10 +5,10 @@
class="site-brand"
to="/"
>
<span class="site-brand-mark">S</span>
<BrandMark class="site-brand-mark" />
<span class="site-brand-heading">
<span class="site-brand-text-wrap">
<span class="site-brand-text">Socialize</span>
<span class="site-brand-text">{{ branding.productName }}</span>
<span
class="site-brand-stage-badge"
:aria-label="t('nav.brandStageLabel')"
@@ -136,6 +136,8 @@
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { productFeatureItems } from '@/static/productFeatures.js';
import { branding } from '@/branding/branding.js';
import BrandMark from '@/components/branding/BrandMark.vue';
const allowedLocales = ['en', 'fr'];
const localeStorageKey = 'user-locale';
@@ -273,13 +275,11 @@
.site-brand {
@apply flex min-w-0 items-start gap-3 no-underline;
color: #172033;
color: var(--h-primary);
}
.site-brand-mark {
@apply flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-2xl text-base font-black;
background: var(--socialize-brand-gradient);
color: #fffaf2;
@apply h-10 w-10 flex-shrink-0 rounded-2xl;
}
.site-brand-heading {

View File

@@ -1,12 +0,0 @@
<template>
<svg fill="currentColor"
viewBox="0 0 540 540"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 0 C2.69849689 0.0031451 5.39587704 -0.02033719 8.09423828 -0.0456543 C24.64095924 -0.10755047 40.6877731 1.83145667 56.59814453 6.50317383 C57.64365479 6.80505615 58.68916504 7.10693848 59.76635742 7.41796875 C92.79656028 17.21576522 120.14877162 34.84217931 145.09814453 58.31567383 C145.80712891 58.97051758 146.51611328 59.62536133 147.24658203 60.30004883 C156.43027452 69.07141226 164.10800524 79.75645166 171.09814453 90.31567383 C171.99726562 91.64985352 171.99726562 91.64985352 172.91455078 93.01098633 C202.24624509 138.40104257 210.21713133 193.46657021 199.45556641 245.92919922 C192.90962056 275.68852135 179.08829353 305.20779517 159.09814453 328.31567383 C158.12705075 329.46844293 157.15708766 330.62216508 156.18798828 331.77661133 C145.74534244 344.15953661 145.74534244 344.15953661 140.09814453 349.31567383 C139.43814453 349.31567383 138.77814453 349.31567383 138.09814453 349.31567383 C138.09814453 349.97567383 138.09814453 350.63567383 138.09814453 351.31567383 C125.46428062 363.48565937 109.61785837 373.30307857 94.09814453 381.31567383 C93.49325195 381.62843262 92.88835937 381.94119141 92.26513672 382.26342773 C64.72703152 396.27155233 34.49385055 402.76232703 3.72314453 402.69067383 C2.84274506 402.69013 1.96234558 402.68958618 1.05526733 402.68902588 C-13.25349056 402.64848426 -26.95433873 401.73425935 -40.90185547 398.31567383 C-42.23821045 397.99421387 -42.23821045 397.99421387 -43.6015625 397.66625977 C-72.07884569 390.61747809 -98.87807183 378.00655878 -121.53857422 359.26879883 C-124.98230298 356.422742 -128.53024689 353.7103623 -132.06591797 350.97973633 C-132.97470703 350.15602539 -132.97470703 350.15602539 -133.90185547 349.31567383 C-133.90185547 348.65567383 -133.90185547 347.99567383 -133.90185547 347.31567383 C-134.56185547 347.31567383 -135.22185547 347.31567383 -135.90185547 347.31567383 C-135.90185547 346.65567383 -135.90185547 345.99567383 -135.90185547 345.31567383 C-136.56185547 345.31567383 -137.22185547 345.31567383 -137.90185547 345.31567383 C-139.24169922 343.98754883 -139.24169922 343.98754883 -140.83935547 342.06567383 C-143.10188476 339.38611053 -145.39815773 336.76489417 -147.80810547 334.21801758 C-148.30834229 333.68660156 -148.8085791 333.15518555 -149.32397461 332.60766602 C-150.32555716 331.54718583 -151.33248849 330.49172924 -152.3449707 329.44165039 C-155.90185547 325.64913211 -155.90185547 325.64913211 -155.90185547 322.31567383 C-151.7526342 322.86293323 -149.09751951 325.32327358 -145.96435547 327.87817383 C-112.49996321 354.28872765 -70.28107399 364.92725762 -28.16088867 360.37231445 C11.7404784 355.37903429 50.51367105 333.83116901 75.28564453 302.00317383 C102.2073581 266.1942078 113.74287629 223.77918143 108.03564453 179.19067383 C103.36182966 146.79964046 88.57557441 116.98148746 66.09814453 93.31567383 C65.60604492 92.78570801 65.11394531 92.25574219 64.60693359 91.7097168 C40.8738918 66.25446714 8.58347239 50.05740641 -25.90185547 45.31567383 C-27.07103516 45.13391602 -28.24021484 44.9521582 -29.44482422 44.76489258 C-71.61533582 39.65438979 -112.89208728 51.7135382 -146.31201172 77.46020508 C-149.8276055 80.21952147 -153.19809546 83.10839876 -156.42138672 86.20629883 C-157.90185547 87.31567383 -157.90185547 87.31567383 -160.90185547 87.31567383 C-160.35570146 83.08040407 -157.80081795 80.22554824 -155.21435547 77.00317383 C-154.72813721 76.3936084 -154.24191895 75.78404297 -153.7409668 75.15600586 C-143.89916607 62.99985508 -133.13277345 52.06245136 -120.90185547 42.31567383 C-120.33450684 41.85741211 -119.7671582 41.39915039 -119.18261719 40.92700195 C-97.55281967 23.63297615 -71.10136924 11.56026197 -44.27685547 5.06567383 C-43.26478027 4.81954346 -42.25270508 4.57341309 -41.20996094 4.31982422 C-27.42871573 1.10789603 -14.10433559 -0.03420308 0 0 Z "
transform="translate(258.90185546875,69.684326171875)"/>
<path
d="M0 0 C23.93418963 20.21356155 38.18828481 47.87411238 43.30859375 78.609375 C43.72796962 83.82104607 43.79621346 89.00908218 43.74609375 94.234375 C43.7423877 94.93147583 43.73868164 95.62857666 43.73486328 96.34680176 C43.51079954 126.08447361 33.84943539 152.98108038 14.30859375 175.609375 C13.37273437 176.74439453 13.37273437 176.74439453 12.41796875 177.90234375 C7.3119221 183.93204517 1.58692987 188.83219983 -4.69140625 193.609375 C-5.65691406 194.351875 -6.62242187 195.094375 -7.6171875 195.859375 C-33.76277727 214.99515608 -66.44544142 222.27976227 -98.37890625 217.421875 C-129.2224086 212.211016 -156.10496998 196.18588064 -175.99609375 172.10546875 C-177.69140625 169.609375 -177.69140625 169.609375 -177.69140625 166.609375 C-176.37140625 166.939375 -175.05140625 167.269375 -173.69140625 167.609375 C-173.69140625 168.269375 -173.69140625 168.929375 -173.69140625 169.609375 C-172.85222656 170.00576172 -172.85222656 170.00576172 -171.99609375 170.41015625 C-169.47801141 171.72041265 -167.18096648 173.22649078 -164.81640625 174.796875 C-141.95286732 189.35191305 -116.13355444 194.94004479 -89.48388672 189.12792969 C-74.00965166 185.43003002 -61.09388018 178.62819028 -48.69140625 168.609375 C-48.05976563 168.14273438 -47.428125 167.67609375 -46.77734375 167.1953125 C-29.95140734 154.19707727 -19.18769244 130.97695501 -16.03515625 110.39453125 C-15.865 109.01587891 -15.865 109.01587891 -15.69140625 107.609375 C-15.55734375 106.55621094 -15.42328125 105.50304687 -15.28515625 104.41796875 C-12.8813248 80.40697048 -19.85309354 55.11934523 -34.69921875 36.02734375 C-35.35664063 35.22941406 -36.0140625 34.43148437 -36.69140625 33.609375 C-37.32820312 32.82175781 -37.965 32.03414062 -38.62109375 31.22265625 C-54.38763523 12.57508008 -77.98681734 0.98006353 -102.19970703 -1.04858398 C-115.33840527 -1.9297872 -128.14870541 -0.56547477 -140.69140625 3.609375 C-141.71621094 3.94710937 -142.74101562 4.28484375 -143.796875 4.6328125 C-155.05737548 8.64192736 -164.81345042 14.73678833 -173.984375 22.359375 C-175.69140625 23.609375 -175.69140625 23.609375 -177.69140625 23.609375 C-176.02661937 12.90007435 -162.17010016 2.50403181 -153.91601562 -3.73193359 C-107.16913616 -37.54750108 -44.81974776 -36.84949238 0 0 Z "
transform="translate(291.69140625,175.390625)"/>
</svg>
</template>

View File

@@ -8,8 +8,6 @@ export default {
theme: {
extend: {
colors: {
hutopyPrimary: "var(--hutopy-primary)",
hutopySecondary: "var(--hutopy-secondary)",
hBackground: "var(--h-background)",
hOnBackground: "var(--h-on-background)",
hSurface: "var(--h-surface)",
@@ -27,4 +25,3 @@ export default {
},
plugins: []
}