Enhance GitHub Workflows and Refactor BFF Code
- Added support for push and workflow_dispatch events in pr-checks.yml to improve CI flexibility. - Implemented concurrency control in workflows to manage job execution more effectively. - Updated pnpm setup to include caching and specified version for consistency. - Removed redundant logging code from BFF application to streamline signal handling and improve readability. - Introduced CSRF_SECRET_KEY validation in environment configuration for production hardening. - Refactored logo component to use a fallback mechanism for image loading, enhancing user experience. - Added linting scripts to package.json for improved code quality checks.
This commit is contained in:
parent
9b2ce83229
commit
84a11f7efc
45
.github/workflows/pr-checks.yml
vendored
45
.github/workflows/pr-checks.yml
vendored
@ -2,9 +2,18 @@ name: Pull Request Checks
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- master
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pr-checks-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
quality-checks:
|
quality-checks:
|
||||||
@ -15,42 +24,34 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.25.0
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
|
cache: "pnpm"
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
id: pnpm-cache
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: pnpm format:check
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm lint
|
run: pnpm lint
|
||||||
|
|
||||||
- name: Run type check
|
- name: Run type check
|
||||||
run: pnpm type-check
|
run: pnpm type-check
|
||||||
|
|
||||||
- name: Run security audit
|
|
||||||
run: pnpm security:check
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Build
|
||||||
run: pnpm format:check
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Verify domain dist is up-to-date
|
||||||
|
run: pnpm domain:check-dist
|
||||||
|
|||||||
12
.github/workflows/security.yml
vendored
12
.github/workflows/security.yml
vendored
@ -97,11 +97,6 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
security-events: write
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: ["javascript", "typescript"]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@ -109,16 +104,13 @@ jobs:
|
|||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: javascript-typescript
|
||||||
queries: security-and-quality
|
queries: security-and-quality
|
||||||
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v3
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:javascript-typescript"
|
||||||
|
|
||||||
outdated-dependencies:
|
outdated-dependencies:
|
||||||
name: Check Outdated Dependencies
|
name: Check Outdated Dependencies
|
||||||
|
|||||||
@ -120,21 +120,6 @@ export async function bootstrap(): Promise<INestApplication> {
|
|||||||
|
|
||||||
// Rely on Nest's built-in shutdown hooks. External orchestrator will send signals.
|
// Rely on Nest's built-in shutdown hooks. External orchestrator will send signals.
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
// #region agent log (hypothesis S1)
|
|
||||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "run1",
|
|
||||||
hypothesisId: "S1",
|
|
||||||
location: "apps/bff/src/app/bootstrap.ts:enableShutdownHooks",
|
|
||||||
message: "Nest shutdown hooks enabled",
|
|
||||||
data: { pid: process.pid },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// API routing prefix is applied via RouterModule in AppModule for clarity and modern routing.
|
// API routing prefix is applied via RouterModule in AppModule for clarity and modern routing.
|
||||||
|
|
||||||
|
|||||||
@ -243,5 +243,12 @@ export function validate(config: Record<string, unknown>): Record<string, unknow
|
|||||||
];
|
];
|
||||||
throw new Error(`Invalid environment configuration: ${messages.join("; ")}`);
|
throw new Error(`Invalid environment configuration: ${messages.join("; ")}`);
|
||||||
}
|
}
|
||||||
|
// Production hardening: CSRF must be backed by a stable secret key.
|
||||||
|
// (If not set, CsrfService generates an ephemeral key which is not suitable for production.)
|
||||||
|
if (result.data.NODE_ENV === "production" && !result.data.CSRF_SECRET_KEY) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid environment configuration: CSRF_SECRET_KEY must be set when NODE_ENV=production"
|
||||||
|
);
|
||||||
|
}
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
import { Injectable, Inject } from "@nestjs/common";
|
import { Injectable, Inject } from "@nestjs/common";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import type { Redis } from "ioredis";
|
import type { Redis } from "ioredis";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
const LOCK_PREFIX = "lock:";
|
const LOCK_PREFIX = "lock:";
|
||||||
const DEFAULT_TTL_MS = 30_000; // 30 seconds
|
const DEFAULT_TTL_MS = 30_000; // 30 seconds
|
||||||
@ -176,7 +177,7 @@ export class DistributedLockService {
|
|||||||
* Generate a unique token for lock ownership
|
* Generate a unique token for lock ownership
|
||||||
*/
|
*/
|
||||||
private generateToken(): string {
|
private generateToken(): string {
|
||||||
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
return randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -44,69 +44,16 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
|
|||||||
|
|
||||||
this.pool = pool;
|
this.pool = pool;
|
||||||
this.instanceTag = `${process.pid}:${Date.now()}`;
|
this.instanceTag = `${process.pid}:${Date.now()}`;
|
||||||
|
|
||||||
// #region agent log (hypothesis S3)
|
|
||||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "run1",
|
|
||||||
hypothesisId: "S3",
|
|
||||||
location: "apps/bff/src/infra/database/prisma.service.ts:constructor",
|
|
||||||
message: "PrismaService constructed",
|
|
||||||
data: { instanceTag: this.instanceTag, pid: process.pid },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.$connect();
|
await this.$connect();
|
||||||
this.logger.log("Database connection established");
|
this.logger.log("Database connection established");
|
||||||
|
|
||||||
// #region agent log (hypothesis S3)
|
|
||||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "run1",
|
|
||||||
hypothesisId: "S3",
|
|
||||||
location: "apps/bff/src/infra/database/prisma.service.ts:onModuleInit",
|
|
||||||
message: "PrismaService connected",
|
|
||||||
data: { instanceTag: this.instanceTag },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
this.destroyCalls += 1;
|
this.destroyCalls += 1;
|
||||||
|
|
||||||
// #region agent log (hypothesis S1)
|
|
||||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "run1",
|
|
||||||
hypothesisId: "S1",
|
|
||||||
location: "apps/bff/src/infra/database/prisma.service.ts:onModuleDestroy(entry)",
|
|
||||||
message: "PrismaService destroy called",
|
|
||||||
data: {
|
|
||||||
instanceTag: this.instanceTag,
|
|
||||||
destroyCalls: this.destroyCalls,
|
|
||||||
poolEnded: this.poolEnded,
|
|
||||||
pid: process.pid,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// Make shutdown idempotent: Nest can call destroy hooks more than once during restarts.
|
// Make shutdown idempotent: Nest can call destroy hooks more than once during restarts.
|
||||||
if (this.poolEnded) {
|
if (this.poolEnded) {
|
||||||
this.logger.warn("Database pool already closed; skipping duplicate shutdown");
|
this.logger.warn("Database pool already closed; skipping duplicate shutdown");
|
||||||
@ -117,25 +64,5 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
|
|||||||
await this.$disconnect();
|
await this.$disconnect();
|
||||||
await this.pool.end();
|
await this.pool.end();
|
||||||
this.logger.log("Database connection closed");
|
this.logger.log("Database connection closed");
|
||||||
|
|
||||||
// #region agent log (hypothesis S1)
|
|
||||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "run1",
|
|
||||||
hypothesisId: "S1",
|
|
||||||
location: "apps/bff/src/infra/database/prisma.service.ts:onModuleDestroy(exit)",
|
|
||||||
message: "PrismaService destroy completed",
|
|
||||||
data: {
|
|
||||||
instanceTag: this.instanceTag,
|
|
||||||
destroyCalls: this.destroyCalls,
|
|
||||||
poolEnded: this.poolEnded,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,21 +11,6 @@ for (const signal of signals) {
|
|||||||
process.once(signal, () => {
|
process.once(signal, () => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
logger.log(`Received ${signal}. Closing Nest application...`);
|
logger.log(`Received ${signal}. Closing Nest application...`);
|
||||||
// #region agent log (hypothesis S1)
|
|
||||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "run1",
|
|
||||||
hypothesisId: "S1",
|
|
||||||
location: "apps/bff/src/main.ts:signalHandler(entry)",
|
|
||||||
message: "Process signal handler invoked",
|
|
||||||
data: { signal, pid: process.pid, appInitialized: !!app },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
if (!app) {
|
if (!app) {
|
||||||
logger.warn("Nest application not initialized. Exiting immediately.");
|
logger.warn("Nest application not initialized. Exiting immediately.");
|
||||||
@ -34,21 +19,6 @@ for (const signal of signals) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// #region agent log (hypothesis S1)
|
|
||||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "run1",
|
|
||||||
hypothesisId: "S1",
|
|
||||||
location: "apps/bff/src/main.ts:signalHandler(beforeClose)",
|
|
||||||
message: "Calling app.close()",
|
|
||||||
data: { signal, pid: process.pid },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
await app.close();
|
await app.close();
|
||||||
logger.log("Nest application closed gracefully.");
|
logger.log("Nest application closed gracefully.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -58,21 +28,6 @@ for (const signal of signals) {
|
|||||||
resolvedError.stack
|
resolvedError.stack
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
// #region agent log (hypothesis S1)
|
|
||||||
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "run1",
|
|
||||||
hypothesisId: "S1",
|
|
||||||
location: "apps/bff/src/main.ts:signalHandler(exit)",
|
|
||||||
message: "Exiting process after shutdown handler",
|
|
||||||
data: { signal, pid: process.pid },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { ZodValidationPipe } from "nestjs-zod";
|
|||||||
import { Public } from "@bff/modules/auth/decorators/public.decorator.js";
|
import { Public } from "@bff/modules/auth/decorators/public.decorator.js";
|
||||||
import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js";
|
import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
import {
|
import {
|
||||||
supportCaseFilterSchema,
|
supportCaseFilterSchema,
|
||||||
createCaseRequestSchema,
|
createCaseRequestSchema,
|
||||||
@ -37,6 +38,11 @@ const publicContactSchema = z.object({
|
|||||||
|
|
||||||
type PublicContactRequest = z.infer<typeof publicContactSchema>;
|
type PublicContactRequest = z.infer<typeof publicContactSchema>;
|
||||||
|
|
||||||
|
const hashEmailForLogs = (email: string): string => {
|
||||||
|
const normalized = email.trim().toLowerCase();
|
||||||
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
||||||
|
};
|
||||||
|
|
||||||
@Controller("support")
|
@Controller("support")
|
||||||
export class SupportController {
|
export class SupportController {
|
||||||
constructor(
|
constructor(
|
||||||
@ -84,7 +90,7 @@ export class SupportController {
|
|||||||
@Body(new ZodValidationPipe(publicContactSchema))
|
@Body(new ZodValidationPipe(publicContactSchema))
|
||||||
body: PublicContactRequest
|
body: PublicContactRequest
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
this.logger.log("Public contact form submission", { email: body.email });
|
this.logger.log("Public contact form submission", { emailHash: hashEmailForLogs(body.email) });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.supportService.createPublicContactRequest(body);
|
await this.supportService.createPublicContactRequest(body);
|
||||||
@ -96,7 +102,7 @@ export class SupportController {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error("Failed to process public contact form", {
|
this.logger.error("Failed to process public contact form", {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
email: body.email,
|
emailHash: hashEmailForLogs(body.email),
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
2
apps/portal/next-env.d.ts
vendored
2
apps/portal/next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@ -9,6 +9,33 @@ interface LogoProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Logo({ className = "", size = 32 }: LogoProps) {
|
export function Logo({ className = "", size = 32 }: LogoProps) {
|
||||||
|
const [fallback, setFallback] = React.useState(false);
|
||||||
|
|
||||||
|
if (fallback) {
|
||||||
|
return (
|
||||||
|
<div className={className} style={{ width: size, height: size }}>
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-full h-full"
|
||||||
|
role="img"
|
||||||
|
aria-label="Assist Solutions Logo"
|
||||||
|
>
|
||||||
|
{/* Top section - Light blue curved arrows */}
|
||||||
|
<path d="M8 8 C12 4, 20 4, 24 8 L20 12 C18 10, 14 10, 12 12 Z" fill="#60A5FA" />
|
||||||
|
<path d="M24 8 C28 12, 28 20, 24 24 L20 20 C22 18, 22 14, 20 12 Z" fill="#60A5FA" />
|
||||||
|
|
||||||
|
{/* Bottom section - Dark blue curved arrows */}
|
||||||
|
<path d="M8 24 C12 28, 20 28, 24 24 L20 20 C18 22, 14 22, 12 20 Z" fill="#1E40AF" />
|
||||||
|
<path d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z" fill="#1E40AF" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} style={{ width: size, height: size }}>
|
<div className={className} style={{ width: size, height: size }}>
|
||||||
<Image
|
<Image
|
||||||
@ -17,43 +44,7 @@ export function Logo({ className = "", size = 32 }: LogoProps) {
|
|||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
className="w-full h-full object-contain"
|
className="w-full h-full object-contain"
|
||||||
onError={e => {
|
onError={() => setFallback(true)}
|
||||||
// Fallback to SVG if image fails to load
|
|
||||||
const target = e.target as HTMLImageElement;
|
|
||||||
target.style.display = "none";
|
|
||||||
const parent = target.parentElement;
|
|
||||||
if (parent) {
|
|
||||||
parent.innerHTML = `
|
|
||||||
<svg
|
|
||||||
width="${size}"
|
|
||||||
height="${size}"
|
|
||||||
viewBox="0 0 32 32"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<!-- Top section - Light blue curved arrows -->
|
|
||||||
<path
|
|
||||||
d="M8 8 C12 4, 20 4, 24 8 L20 12 C18 10, 14 10, 12 12 Z"
|
|
||||||
fill="#60A5FA"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M24 8 C28 12, 28 20, 24 24 L20 20 C22 18, 22 14, 20 12 Z"
|
|
||||||
fill="#60A5FA"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Bottom section - Dark blue curved arrows -->
|
|
||||||
<path
|
|
||||||
d="M8 24 C12 28, 20 28, 24 24 L20 20 C18 22, 14 22, 12 20 Z"
|
|
||||||
fill="#1E40AF"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z"
|
|
||||||
fill="#1E40AF"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -169,6 +169,9 @@
|
|||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "tsc -w --preserveWatchOutput",
|
"dev": "tsc -w --preserveWatchOutput",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"test": "echo 'No tests yet'",
|
||||||
"type-check": "NODE_OPTIONS=\"--max-old-space-size=2048 --max-semi-space-size=128\" tsc --project tsconfig.json --noEmit",
|
"type-check": "NODE_OPTIONS=\"--max-old-space-size=2048 --max-semi-space-size=128\" tsc --project tsconfig.json --noEmit",
|
||||||
"typecheck": "pnpm run type-check"
|
"typecheck": "pnpm run type-check"
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user