added qr, cleaned up profile views, and added the critical alerts
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
|||||||
deleteMedicationForBird,
|
deleteMedicationForBird,
|
||||||
deleteVetVisitForBird,
|
deleteVetVisitForBird,
|
||||||
getBirdById,
|
getBirdById,
|
||||||
|
getBirdByPublicProfileCode,
|
||||||
listBirds,
|
listBirds,
|
||||||
listDueBirdMilestoneReminders,
|
listDueBirdMilestoneReminders,
|
||||||
listMemorializedBirds,
|
listMemorializedBirds,
|
||||||
@@ -231,6 +232,8 @@ const lostBirdReportSchema = z.object({
|
|||||||
message: z.string().trim().max(1000).optional().or(z.literal('')),
|
message: z.string().trim().max(1000).optional().or(z.literal('')),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const publicProfileCodeSchema = z.string().trim().regex(/^[A-Za-z0-9_-]{8,32}$/);
|
||||||
|
|
||||||
const birdSchema = z.object({
|
const birdSchema = z.object({
|
||||||
name: z.string().trim().min(1).max(120),
|
name: z.string().trim().min(1).max(120),
|
||||||
tagId: z.string().trim().max(80).optional().or(z.literal('')),
|
tagId: z.string().trim().max(80).optional().or(z.literal('')),
|
||||||
@@ -245,6 +248,7 @@ const birdSchema = z.object({
|
|||||||
photoDataUrl: z.union([photoDataUrlSchema, photoUrlSchema, z.literal('')]).optional(),
|
photoDataUrl: z.union([photoDataUrlSchema, photoUrlSchema, z.literal('')]).optional(),
|
||||||
notifyOnDob: z.boolean().optional(),
|
notifyOnDob: z.boolean().optional(),
|
||||||
notifyOnGotchaDay: z.boolean().optional(),
|
notifyOnGotchaDay: z.boolean().optional(),
|
||||||
|
publicProfileEnabled: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const memorializeBirdSchema = z.object({
|
const memorializeBirdSchema = z.object({
|
||||||
@@ -325,6 +329,7 @@ const hashToken = (token: string) => crypto.createHash('sha256').update(token).d
|
|||||||
const createSessionToken = () => crypto.randomBytes(32).toString('hex');
|
const createSessionToken = () => crypto.randomBytes(32).toString('hex');
|
||||||
const photoAccessSecret = process.env.PHOTO_ACCESS_SECRET?.trim() || process.env.S3_SECRET_ACCESS_KEY?.trim() || createSessionToken();
|
const photoAccessSecret = process.env.PHOTO_ACCESS_SECRET?.trim() || process.env.S3_SECRET_ACCESS_KEY?.trim() || createSessionToken();
|
||||||
const createIntegrationToken = () => `flpt_${crypto.randomBytes(24).toString('hex')}`;
|
const createIntegrationToken = () => `flpt_${crypto.randomBytes(24).toString('hex')}`;
|
||||||
|
const createPublicProfileCode = () => crypto.randomBytes(9).toString('base64url');
|
||||||
const createRandomId = () => crypto.randomUUID();
|
const createRandomId = () => crypto.randomUUID();
|
||||||
const createCodeVerifier = () => crypto.randomBytes(32).toString('base64url');
|
const createCodeVerifier = () => crypto.randomBytes(32).toString('base64url');
|
||||||
const createCodeChallenge = (verifier: string) => crypto.createHash('sha256').update(verifier).digest('base64url');
|
const createCodeChallenge = (verifier: string) => crypto.createHash('sha256').update(verifier).digest('base64url');
|
||||||
@@ -579,6 +584,8 @@ const normalizeBird = (row: BirdRow) => ({
|
|||||||
photoUpdatedAt: row.photo_updated_at,
|
photoUpdatedAt: row.photo_updated_at,
|
||||||
notifyOnDob: row.notify_on_dob,
|
notifyOnDob: row.notify_on_dob,
|
||||||
notifyOnGotchaDay: row.notify_on_gotcha_day,
|
notifyOnGotchaDay: row.notify_on_gotcha_day,
|
||||||
|
publicProfileCode: row.public_profile_code ?? null,
|
||||||
|
publicProfileEnabled: row.public_profile_enabled ?? false,
|
||||||
memorializedAt: row.memorialized_at,
|
memorializedAt: row.memorialized_at,
|
||||||
memorializedOn: row.memorialized_on,
|
memorializedOn: row.memorialized_on,
|
||||||
memorialNote: row.memorial_note,
|
memorialNote: row.memorial_note,
|
||||||
@@ -588,6 +595,15 @@ const normalizeBird = (row: BirdRow) => ({
|
|||||||
latestRecordedOn: row.latest_recorded_on,
|
latestRecordedOn: row.latest_recorded_on,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const normalizePublicBirdProfile = (row: BirdRow) => ({
|
||||||
|
id: row.id,
|
||||||
|
workspaceId: row.workspace_id,
|
||||||
|
name: row.name,
|
||||||
|
gender: row.gender,
|
||||||
|
dateOfBirth: row.date_of_birth,
|
||||||
|
photoDataUrl: getBirdPhotoUrl(row),
|
||||||
|
});
|
||||||
|
|
||||||
const normalizeWeight = (row: WeightRow) => ({
|
const normalizeWeight = (row: WeightRow) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
birdId: row.bird_id,
|
birdId: row.bird_id,
|
||||||
@@ -1949,6 +1965,28 @@ app.post('/api/lost-bird/report', lostBirdReportLimiter, async (req: Request, re
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/public/birds/:publicProfileCode', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const parsed = publicProfileCodeSchema.safeParse(req.params.publicProfileCode);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(404).json({ error: 'Public bird profile not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bird = await getBirdByPublicProfileCode(parsed.data);
|
||||||
|
|
||||||
|
if (!bird) {
|
||||||
|
res.status(404).json({ error: 'Public bird profile not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ bird: normalizePublicBirdProfile(bird) });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/auth/providers', (_req: Request, res: Response) => {
|
app.get('/api/auth/providers', (_req: Request, res: Response) => {
|
||||||
res.json({
|
res.json({
|
||||||
providers: Object.values(oauthProviders).map((provider) => ({
|
providers: Object.values(oauthProviders).map((provider) => ({
|
||||||
@@ -2858,6 +2896,8 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
|
|||||||
photoUpdatedAt: photoStorage.photoUpdatedAt,
|
photoUpdatedAt: photoStorage.photoUpdatedAt,
|
||||||
notifyOnDob: parsed.data.notifyOnDob ?? false,
|
notifyOnDob: parsed.data.notifyOnDob ?? false,
|
||||||
notifyOnGotchaDay: parsed.data.notifyOnGotchaDay ?? false,
|
notifyOnGotchaDay: parsed.data.notifyOnGotchaDay ?? false,
|
||||||
|
publicProfileCode: createPublicProfileCode(),
|
||||||
|
publicProfileEnabled: parsed.data.publicProfileEnabled ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
uploadedObjectKeyToCleanup = null;
|
uploadedObjectKeyToCleanup = null;
|
||||||
@@ -2998,6 +3038,8 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
|
|||||||
photoUpdatedAt: photoStorage.photoUpdatedAt,
|
photoUpdatedAt: photoStorage.photoUpdatedAt,
|
||||||
notifyOnDob: parsed.data.notifyOnDob ?? false,
|
notifyOnDob: parsed.data.notifyOnDob ?? false,
|
||||||
notifyOnGotchaDay: parsed.data.notifyOnGotchaDay ?? false,
|
notifyOnGotchaDay: parsed.data.notifyOnGotchaDay ?? false,
|
||||||
|
publicProfileCode: existingBird.public_profile_code ?? createPublicProfileCode(),
|
||||||
|
publicProfileEnabled: parsed.data.publicProfileEnabled ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!bird) {
|
if (!bird) {
|
||||||
|
|||||||
@@ -225,6 +225,8 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
|||||||
photo_updated_at TIMESTAMPTZ,
|
photo_updated_at TIMESTAMPTZ,
|
||||||
notify_on_dob BOOLEAN NOT NULL DEFAULT FALSE,
|
notify_on_dob BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
notify_on_gotcha_day BOOLEAN NOT NULL DEFAULT FALSE,
|
notify_on_gotcha_day BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
public_profile_code VARCHAR(32),
|
||||||
|
public_profile_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
memorialized_at TIMESTAMPTZ,
|
memorialized_at TIMESTAMPTZ,
|
||||||
memorialized_on DATE,
|
memorialized_on DATE,
|
||||||
memorial_note VARCHAR(1000),
|
memorial_note VARCHAR(1000),
|
||||||
@@ -247,6 +249,8 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
|||||||
ADD COLUMN IF NOT EXISTS photo_updated_at TIMESTAMPTZ,
|
ADD COLUMN IF NOT EXISTS photo_updated_at TIMESTAMPTZ,
|
||||||
ADD COLUMN IF NOT EXISTS notify_on_dob BOOLEAN NOT NULL DEFAULT FALSE,
|
ADD COLUMN IF NOT EXISTS notify_on_dob BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
ADD COLUMN IF NOT EXISTS notify_on_gotcha_day BOOLEAN NOT NULL DEFAULT FALSE,
|
ADD COLUMN IF NOT EXISTS notify_on_gotcha_day BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS public_profile_code VARCHAR(32),
|
||||||
|
ADD COLUMN IF NOT EXISTS public_profile_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
ADD COLUMN IF NOT EXISTS memorialized_at TIMESTAMPTZ,
|
ADD COLUMN IF NOT EXISTS memorialized_at TIMESTAMPTZ,
|
||||||
ADD COLUMN IF NOT EXISTS memorialized_on DATE,
|
ADD COLUMN IF NOT EXISTS memorialized_on DATE,
|
||||||
ADD COLUMN IF NOT EXISTS memorial_note VARCHAR(1000),
|
ADD COLUMN IF NOT EXISTS memorial_note VARCHAR(1000),
|
||||||
@@ -305,6 +309,10 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
|||||||
ON birds (photo_object_key)
|
ON birds (photo_object_key)
|
||||||
WHERE photo_object_key IS NOT NULL;
|
WHERE photo_object_key IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_birds_public_profile_code
|
||||||
|
ON birds (public_profile_code)
|
||||||
|
WHERE public_profile_code IS NOT NULL;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pending_bird_transfers (
|
CREATE TABLE IF NOT EXISTS pending_bird_transfers (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
|
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const birdSelectFields = `
|
|||||||
birds.photo_updated_at,
|
birds.photo_updated_at,
|
||||||
birds.notify_on_dob,
|
birds.notify_on_dob,
|
||||||
birds.notify_on_gotcha_day,
|
birds.notify_on_gotcha_day,
|
||||||
|
birds.public_profile_code,
|
||||||
|
birds.public_profile_enabled,
|
||||||
birds.memorialized_at,
|
birds.memorialized_at,
|
||||||
birds.memorialized_on::text,
|
birds.memorialized_on::text,
|
||||||
birds.memorial_note,
|
birds.memorial_note,
|
||||||
@@ -62,6 +64,27 @@ export const getBirdById = async (birdId: string, workspaceId: number) => {
|
|||||||
return result.rows[0] ?? null;
|
return result.rows[0] ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getBirdByPublicProfileCode = async (publicProfileCode: string) => {
|
||||||
|
const result = await db.query<BirdRow>(
|
||||||
|
`SELECT
|
||||||
|
${birdSelectFields}
|
||||||
|
FROM birds
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT weight_grams, recorded_on
|
||||||
|
FROM weight_records
|
||||||
|
WHERE weight_records.bird_id = birds.id
|
||||||
|
ORDER BY recorded_on DESC
|
||||||
|
LIMIT 1
|
||||||
|
) latest ON TRUE
|
||||||
|
WHERE birds.public_profile_code = $1
|
||||||
|
AND birds.public_profile_enabled = TRUE
|
||||||
|
AND birds.memorialized_at IS NULL`,
|
||||||
|
[publicProfileCode],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows[0] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
export const listBirds = async (workspaceId: number) => {
|
export const listBirds = async (workspaceId: number) => {
|
||||||
const result = await db.query<BirdRow>(
|
const result = await db.query<BirdRow>(
|
||||||
`SELECT
|
`SELECT
|
||||||
@@ -274,6 +297,8 @@ export const createBird = async ({
|
|||||||
photoUpdatedAt = null,
|
photoUpdatedAt = null,
|
||||||
notifyOnDob,
|
notifyOnDob,
|
||||||
notifyOnGotchaDay,
|
notifyOnGotchaDay,
|
||||||
|
publicProfileCode = null,
|
||||||
|
publicProfileEnabled = false,
|
||||||
}: {
|
}: {
|
||||||
birdId?: string;
|
birdId?: string;
|
||||||
workspaceId: number;
|
workspaceId: number;
|
||||||
@@ -293,11 +318,13 @@ export const createBird = async ({
|
|||||||
photoUpdatedAt?: string | null;
|
photoUpdatedAt?: string | null;
|
||||||
notifyOnDob: boolean;
|
notifyOnDob: boolean;
|
||||||
notifyOnGotchaDay: boolean;
|
notifyOnGotchaDay: boolean;
|
||||||
|
publicProfileCode?: string | null;
|
||||||
|
publicProfileEnabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const result = await db.query<BirdRow>(
|
const result = await db.query<BirdRow>(
|
||||||
`INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day)
|
`INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled)
|
||||||
VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
|
||||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
|
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
|
||||||
[
|
[
|
||||||
birdId ?? null,
|
birdId ?? null,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -317,6 +344,8 @@ export const createBird = async ({
|
|||||||
photoUpdatedAt,
|
photoUpdatedAt,
|
||||||
notifyOnDob,
|
notifyOnDob,
|
||||||
notifyOnGotchaDay,
|
notifyOnGotchaDay,
|
||||||
|
publicProfileCode,
|
||||||
|
publicProfileEnabled,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -342,6 +371,8 @@ export const updateBird = async ({
|
|||||||
photoUpdatedAt = null,
|
photoUpdatedAt = null,
|
||||||
notifyOnDob,
|
notifyOnDob,
|
||||||
notifyOnGotchaDay,
|
notifyOnGotchaDay,
|
||||||
|
publicProfileCode,
|
||||||
|
publicProfileEnabled,
|
||||||
}: {
|
}: {
|
||||||
birdId: string;
|
birdId: string;
|
||||||
workspaceId: number;
|
workspaceId: number;
|
||||||
@@ -361,6 +392,8 @@ export const updateBird = async ({
|
|||||||
photoUpdatedAt?: string | null;
|
photoUpdatedAt?: string | null;
|
||||||
notifyOnDob: boolean;
|
notifyOnDob: boolean;
|
||||||
notifyOnGotchaDay: boolean;
|
notifyOnGotchaDay: boolean;
|
||||||
|
publicProfileCode: string | null;
|
||||||
|
publicProfileEnabled: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const result = await db.query<BirdRow>(
|
const result = await db.query<BirdRow>(
|
||||||
`UPDATE birds
|
`UPDATE birds
|
||||||
@@ -379,11 +412,13 @@ export const updateBird = async ({
|
|||||||
photo_content_type = $14,
|
photo_content_type = $14,
|
||||||
photo_updated_at = $15,
|
photo_updated_at = $15,
|
||||||
notify_on_dob = $16,
|
notify_on_dob = $16,
|
||||||
notify_on_gotcha_day = $17
|
notify_on_gotcha_day = $17,
|
||||||
|
public_profile_code = $18,
|
||||||
|
public_profile_enabled = $19
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND workspace_id = $18
|
AND workspace_id = $20
|
||||||
AND memorialized_at IS NULL
|
AND memorialized_at IS NULL
|
||||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||||
(
|
(
|
||||||
SELECT weight_grams::text
|
SELECT weight_grams::text
|
||||||
FROM weight_records
|
FROM weight_records
|
||||||
@@ -416,6 +451,8 @@ export const updateBird = async ({
|
|||||||
photoUpdatedAt,
|
photoUpdatedAt,
|
||||||
notifyOnDob,
|
notifyOnDob,
|
||||||
notifyOnGotchaDay,
|
notifyOnGotchaDay,
|
||||||
|
publicProfileCode,
|
||||||
|
publicProfileEnabled,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ export type BirdRow = {
|
|||||||
photo_updated_at: string | null;
|
photo_updated_at: string | null;
|
||||||
notify_on_dob: boolean;
|
notify_on_dob: boolean;
|
||||||
notify_on_gotcha_day: boolean;
|
notify_on_gotcha_day: boolean;
|
||||||
|
public_profile_code: string | null;
|
||||||
|
public_profile_enabled: boolean;
|
||||||
memorialized_at: string | null;
|
memorialized_at: string | null;
|
||||||
memorialized_on: string | null;
|
memorialized_on: string | null;
|
||||||
memorial_note: string | null;
|
memorial_note: string | null;
|
||||||
|
|||||||
Generated
+331
@@ -8,6 +8,8 @@
|
|||||||
"name": "flockpal-frontend",
|
"name": "flockpal-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "18.3.1"
|
||||||
},
|
},
|
||||||
@@ -1144,6 +1146,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||||
|
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": ">=7.24.0 <7.24.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.15",
|
"version": "15.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
@@ -1151,6 +1162,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qrcode": {
|
||||||
|
"version": "1.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
|
||||||
|
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.12",
|
"version": "18.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
|
||||||
@@ -1192,6 +1212,30 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.16",
|
"version": "2.10.16",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
|
||||||
@@ -1239,6 +1283,15 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelcase": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001786",
|
"version": "1.0.30001786",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz",
|
||||||
@@ -1260,6 +1313,35 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
@@ -1292,6 +1374,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.331",
|
"version": "1.5.331",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
|
||||||
@@ -1299,6 +1396,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||||
@@ -1348,6 +1451,19 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -1373,6 +1489,24 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -1405,6 +1539,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -1460,6 +1606,51 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-exists": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1467,6 +1658,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.8",
|
"version": "8.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||||
@@ -1496,6 +1696,23 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
@@ -1531,6 +1748,21 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.60.1",
|
"version": "4.60.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
||||||
@@ -1595,6 +1827,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -1605,6 +1843,32 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.6.3",
|
"version": "5.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||||
@@ -1619,6 +1883,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.24.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||||
|
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||||
@@ -1710,12 +1980,73 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "18.3.1"
|
||||||
},
|
},
|
||||||
|
|||||||
+248
-52
@@ -1,7 +1,9 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import birdSilhouette from './assets/bird-silhouette.jpg';
|
||||||
import flockPalLandingArt from './assets/flockpal-landing-art.png';
|
import flockPalLandingArt from './assets/flockpal-landing-art.png';
|
||||||
import defaultBirdPhoto from './assets/yoda-default.png';
|
import defaultBirdPhoto from './assets/yoda-default.png';
|
||||||
import { findParrotWeightReference, parrotSpeciesOptions, type ParrotWeightReference } from './parrotWeightReference';
|
import { findParrotWeightReference, parrotSpeciesOptions, type ParrotWeightReference } from './parrotWeightReference';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
|
||||||
type BillingPlan = 'rescue_free' | 'household_basic' | 'household_plus' | 'household_macaw';
|
type BillingPlan = 'rescue_free' | 'household_basic' | 'household_plus' | 'household_macaw';
|
||||||
type HouseholdBillingPlan = Exclude<BillingPlan, 'rescue_free'>;
|
type HouseholdBillingPlan = Exclude<BillingPlan, 'rescue_free'>;
|
||||||
@@ -29,6 +31,8 @@ type Bird = {
|
|||||||
photoDataUrl: string | null;
|
photoDataUrl: string | null;
|
||||||
notifyOnDob: boolean;
|
notifyOnDob: boolean;
|
||||||
notifyOnGotchaDay: boolean;
|
notifyOnGotchaDay: boolean;
|
||||||
|
publicProfileCode: string | null;
|
||||||
|
publicProfileEnabled: boolean;
|
||||||
memorializedAt: string | null;
|
memorializedAt: string | null;
|
||||||
memorializedOn: string | null;
|
memorializedOn: string | null;
|
||||||
memorialNote: string | null;
|
memorialNote: string | null;
|
||||||
@@ -192,6 +196,16 @@ type BirdFormState = {
|
|||||||
photoDataUrl: string;
|
photoDataUrl: string;
|
||||||
notifyOnDob: boolean;
|
notifyOnDob: boolean;
|
||||||
notifyOnGotchaDay: boolean;
|
notifyOnGotchaDay: boolean;
|
||||||
|
publicProfileEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PublicBirdProfile = {
|
||||||
|
id: string;
|
||||||
|
workspaceId: number;
|
||||||
|
name: string;
|
||||||
|
gender: BirdGender;
|
||||||
|
dateOfBirth: string | null;
|
||||||
|
photoDataUrl: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MemorializeBirdFormState = {
|
type MemorializeBirdFormState = {
|
||||||
@@ -317,6 +331,47 @@ type SettingsSection = 'collaborators' | 'integration-tokens' | 'new-workspace'
|
|||||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:5000/api';
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:5000/api';
|
||||||
const sessionTokenStorageKey = 'flockpal_auth_token';
|
const sessionTokenStorageKey = 'flockpal_auth_token';
|
||||||
const dismissedAlertsStorageKey = 'flockpal_dismissed_alerts';
|
const dismissedAlertsStorageKey = 'flockpal_dismissed_alerts';
|
||||||
|
const getPublicProfileCodeFromPath = () => window.location.pathname.match(/^\/b\/([A-Za-z0-9_-]{8,32})\/?$/)?.[1] ?? '';
|
||||||
|
const getPublicProfileUrl = (code: string) => `${window.location.origin}/b/${code}`;
|
||||||
|
const QR_MARGIN = 4;
|
||||||
|
|
||||||
|
const createQrPath = (value: string) => {
|
||||||
|
const qr = QRCode.create(value, { errorCorrectionLevel: 'H' });
|
||||||
|
const size = qr.modules.size;
|
||||||
|
const data = qr.modules.data;
|
||||||
|
const pathParts: string[] = [];
|
||||||
|
|
||||||
|
for (let y = 0; y < size; y += 1) {
|
||||||
|
for (let x = 0; x < size; x += 1) {
|
||||||
|
if (data[y * size + x]) {
|
||||||
|
pathParts.push(`M${x + QR_MARGIN},${y + QR_MARGIN}h1v1h-1z`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: pathParts.join(''),
|
||||||
|
size,
|
||||||
|
viewBoxSize: size + QR_MARGIN * 2,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const QrCodeWithLogo = ({ value, label }: { value: string; label: string }) => {
|
||||||
|
const qr = useMemo(() => createQrPath(value), [value]);
|
||||||
|
const logoSize = Math.max(7, qr.size * 0.18);
|
||||||
|
const logoPosition = (qr.viewBoxSize - logoSize) / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg className="qr-code" viewBox={`0 0 ${qr.viewBoxSize} ${qr.viewBoxSize}`} role="img" aria-label={label}>
|
||||||
|
<rect width={qr.viewBoxSize} height={qr.viewBoxSize} fill="#fff" />
|
||||||
|
<path d={qr.path} fill="#111418" />
|
||||||
|
<g className="qr-bird-mark" aria-hidden="true">
|
||||||
|
<rect x={logoPosition - 0.45} y={logoPosition - 0.45} width={logoSize + 0.9} height={logoSize + 0.9} rx="1.7" />
|
||||||
|
<image href={birdSilhouette} x={logoPosition} y={logoPosition} width={logoSize} height={logoSize} preserveAspectRatio="xMidYMid meet" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
const emptyBirdForm: BirdFormState = {
|
const emptyBirdForm: BirdFormState = {
|
||||||
name: '',
|
name: '',
|
||||||
tagId: '',
|
tagId: '',
|
||||||
@@ -331,6 +386,7 @@ const emptyBirdForm: BirdFormState = {
|
|||||||
photoDataUrl: '',
|
photoDataUrl: '',
|
||||||
notifyOnDob: false,
|
notifyOnDob: false,
|
||||||
notifyOnGotchaDay: false,
|
notifyOnGotchaDay: false,
|
||||||
|
publicProfileEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyMemorializeBirdForm = (): MemorializeBirdFormState => ({
|
const emptyMemorializeBirdForm = (): MemorializeBirdFormState => ({
|
||||||
@@ -456,6 +512,7 @@ const toBirdForm = (bird: Bird): BirdFormState => ({
|
|||||||
photoDataUrl: bird.photoDataUrl ?? '',
|
photoDataUrl: bird.photoDataUrl ?? '',
|
||||||
notifyOnDob: bird.notifyOnDob,
|
notifyOnDob: bird.notifyOnDob,
|
||||||
notifyOnGotchaDay: bird.notifyOnGotchaDay,
|
notifyOnGotchaDay: bird.notifyOnGotchaDay,
|
||||||
|
publicProfileEnabled: bird.publicProfileEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (value: string | null) => {
|
const formatDate = (value: string | null) => {
|
||||||
@@ -1117,6 +1174,10 @@ function App() {
|
|||||||
const [lostBirdReportForm, setLostBirdReportForm] = useState<LostBirdReportFormState>(emptyLostBirdReportForm);
|
const [lostBirdReportForm, setLostBirdReportForm] = useState<LostBirdReportFormState>(emptyLostBirdReportForm);
|
||||||
const [lostBirdReportNotice, setLostBirdReportNotice] = useState<{ message: string; kind: 'success' | 'error' } | null>(null);
|
const [lostBirdReportNotice, setLostBirdReportNotice] = useState<{ message: string; kind: 'success' | 'error' } | null>(null);
|
||||||
const [lostBirdReportSubmitting, setLostBirdReportSubmitting] = useState(false);
|
const [lostBirdReportSubmitting, setLostBirdReportSubmitting] = useState(false);
|
||||||
|
const [publicProfileCode] = useState(getPublicProfileCodeFromPath);
|
||||||
|
const [publicProfile, setPublicProfile] = useState<PublicBirdProfile | null>(null);
|
||||||
|
const [publicProfileLoading, setPublicProfileLoading] = useState(Boolean(getPublicProfileCodeFromPath()));
|
||||||
|
const [publicProfileError, setPublicProfileError] = useState('');
|
||||||
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
||||||
const [activeMembership, setActiveMembership] = useState<WorkspaceMember | null>(null);
|
const [activeMembership, setActiveMembership] = useState<WorkspaceMember | null>(null);
|
||||||
const [workspaceMembers, setWorkspaceMembers] = useState<WorkspaceMember[]>([]);
|
const [workspaceMembers, setWorkspaceMembers] = useState<WorkspaceMember[]>([]);
|
||||||
@@ -1159,6 +1220,7 @@ function App() {
|
|||||||
const [updatingRescueWorkspaceId, setUpdatingRescueWorkspaceId] = useState<number | null>(null);
|
const [updatingRescueWorkspaceId, setUpdatingRescueWorkspaceId] = useState<number | null>(null);
|
||||||
const [switchingWorkspaceId, setSwitchingWorkspaceId] = useState<number | null>(null);
|
const [switchingWorkspaceId, setSwitchingWorkspaceId] = useState<number | null>(null);
|
||||||
const [showWeightAlertModal, setShowWeightAlertModal] = useState(false);
|
const [showWeightAlertModal, setShowWeightAlertModal] = useState(false);
|
||||||
|
const [qrBird, setQrBird] = useState<Bird | null>(null);
|
||||||
const [speciesPickerOpen, setSpeciesPickerOpen] = useState(false);
|
const [speciesPickerOpen, setSpeciesPickerOpen] = useState(false);
|
||||||
const [bulkWeightOpen, setBulkWeightOpen] = useState(false);
|
const [bulkWeightOpen, setBulkWeightOpen] = useState(false);
|
||||||
const [savingBulkWeights, setSavingBulkWeights] = useState(false);
|
const [savingBulkWeights, setSavingBulkWeights] = useState(false);
|
||||||
@@ -1236,6 +1298,16 @@ function App() {
|
|||||||
|
|
||||||
const showFlockDetailColumn = bulkWeightOpen || Boolean(selectedBird);
|
const showFlockDetailColumn = bulkWeightOpen || Boolean(selectedBird);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!publicProfile || !authSession || workspace?.id !== publicProfile.workspaceId || !birds.some((bird) => bird.id === publicProfile.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedBirdId(publicProfile.id);
|
||||||
|
setActivePage('flock');
|
||||||
|
window.history.replaceState({}, document.title, '/');
|
||||||
|
}, [authSession, birds, publicProfile, workspace?.id]);
|
||||||
|
|
||||||
const missingFirstWeightCount = useMemo(
|
const missingFirstWeightCount = useMemo(
|
||||||
() => birds.filter((bird) => bird.latestWeightGrams === null).length,
|
() => birds.filter((bird) => bird.latestWeightGrams === null).length,
|
||||||
[birds],
|
[birds],
|
||||||
@@ -1394,8 +1466,6 @@ function App() {
|
|||||||
[activeVetVisitDueBirds, allBirdVetVisits, dismissedAlerts, workspace?.id],
|
[activeVetVisitDueBirds, allBirdVetVisits, dismissedAlerts, workspace?.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
const vetVisitDueNames = vetVisitDueBirds.slice(0, 3).map((bird) => bird.name).join(', ');
|
|
||||||
const vetVisitDueOverflowCount = Math.max(vetVisitDueBirds.length - 3, 0);
|
|
||||||
const vetVisitDueBirdIds = useMemo(() => new Set(vetVisitDueBirds.map((bird) => bird.id)), [vetVisitDueBirds]);
|
const vetVisitDueBirdIds = useMemo(() => new Set(vetVisitDueBirds.map((bird) => bird.id)), [vetVisitDueBirds]);
|
||||||
|
|
||||||
const activeMedications = useMemo(
|
const activeMedications = useMemo(
|
||||||
@@ -1785,6 +1855,37 @@ function App() {
|
|||||||
void bootstrapSession();
|
void bootstrapSession();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!publicProfileCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPublicProfile = async () => {
|
||||||
|
try {
|
||||||
|
setPublicProfileLoading(true);
|
||||||
|
setPublicProfileError('');
|
||||||
|
const response = await apiFetch(`/public/birds/${publicProfileCode}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await readErrorMessage(response, 'Public bird profile not found.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await readJsonSafely<{ bird?: PublicBirdProfile }>(response)) ?? {};
|
||||||
|
if (!data.bird) {
|
||||||
|
throw new Error('Public bird profile not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setPublicProfile(data.bird);
|
||||||
|
} catch (profileError) {
|
||||||
|
setPublicProfileError(profileError instanceof Error ? profileError.message : 'Public bird profile not found.');
|
||||||
|
} finally {
|
||||||
|
setPublicProfileLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadPublicProfile();
|
||||||
|
}, [publicProfileCode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authToken || !workspace?.id) {
|
if (!authToken || !workspace?.id) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -2150,6 +2251,21 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenPublicProfileBird = async () => {
|
||||||
|
if (!publicProfile || !authSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workspace?.id !== publicProfile.workspaceId) {
|
||||||
|
await handleWorkspaceSwitch(publicProfile.workspaceId, 'flock');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedBirdId(publicProfile.id);
|
||||||
|
setActivePage('flock');
|
||||||
|
window.history.replaceState({}, document.title, '/');
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateIntegrationToken = async (event: React.FormEvent<HTMLFormElement>) => {
|
const handleCreateIntegrationToken = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -3457,6 +3573,55 @@ function App() {
|
|||||||
setActivePage('flock');
|
setActivePage('flock');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const publicProfileWorkspaceMembership = publicProfile
|
||||||
|
? authSession?.workspaces.find((entry) => entry.workspace.id === publicProfile.workspaceId) ?? null
|
||||||
|
: null;
|
||||||
|
const shouldShowPublicProfilePage =
|
||||||
|
Boolean(publicProfileCode) &&
|
||||||
|
(!authSession ||
|
||||||
|
!publicProfile ||
|
||||||
|
workspace?.id !== publicProfile.workspaceId ||
|
||||||
|
!birds.some((bird) => bird.id === publicProfile.id));
|
||||||
|
|
||||||
|
if (shouldShowPublicProfilePage) {
|
||||||
|
return (
|
||||||
|
<main className="auth-shell public-profile-shell">
|
||||||
|
<section className="panel public-profile-card">
|
||||||
|
{publicProfileLoading || authLoading ? (
|
||||||
|
<p>Loading bird profile...</p>
|
||||||
|
) : publicProfileError || !publicProfile ? (
|
||||||
|
<>
|
||||||
|
<p className="eyebrow">FlockPal</p>
|
||||||
|
<h1>Public profile unavailable</h1>
|
||||||
|
<p className="muted">{publicProfileError || 'This bird profile is not available publicly.'}</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<img className="public-profile-photo" src={publicProfile.photoDataUrl || defaultBirdPhoto} alt={publicProfile.name} />
|
||||||
|
<div className="public-profile-copy">
|
||||||
|
<h1>
|
||||||
|
<span>{publicProfile.name}</span>
|
||||||
|
<span aria-label={getBirdGenderLabel(publicProfile)} className={`gender-symbol ${publicProfile.gender}`}>
|
||||||
|
{getBirdGenderSymbol(publicProfile)}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<article className="summary-card">
|
||||||
|
<span>Hatch Day</span>
|
||||||
|
<strong>{formatDate(publicProfile.dateOfBirth)}</strong>
|
||||||
|
</article>
|
||||||
|
{publicProfileWorkspaceMembership ? (
|
||||||
|
<button className="primary-button" onClick={handleOpenPublicProfileBird} type="button">
|
||||||
|
Open full profile
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (authLoading) {
|
if (authLoading) {
|
||||||
return (
|
return (
|
||||||
<main className="auth-shell">
|
<main className="auth-shell">
|
||||||
@@ -3770,6 +3935,35 @@ function App() {
|
|||||||
|
|
||||||
<section className="content-shell">
|
<section className="content-shell">
|
||||||
{error ? <p className="error-banner">{error}</p> : null}
|
{error ? <p className="error-banner">{error}</p> : null}
|
||||||
|
{(activePage === 'overview' || activePage === 'flock') && (totalWeightAlerts || vetVisitDueBirds.length) ? (
|
||||||
|
<section className="top-alert-notification" role="alert" aria-label="Critical flock alert">
|
||||||
|
<span className="notification-bell" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<strong>
|
||||||
|
{totalWeightAlerts + vetVisitDueBirds.length} critical alert{totalWeightAlerts + vetVisitDueBirds.length === 1 ? '' : 's'}
|
||||||
|
</strong>
|
||||||
|
<span>
|
||||||
|
{totalWeightAlerts ? `${totalWeightAlerts} weight alert${totalWeightAlerts === 1 ? '' : 's'}` : ''}
|
||||||
|
{totalWeightAlerts && vetVisitDueBirds.length ? ' • ' : ''}
|
||||||
|
{vetVisitDueBirds.length
|
||||||
|
? `${vetVisitDueBirds.length} annual vet reminder${vetVisitDueBirds.length === 1 ? '' : 's'}`
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="top-alert-actions">
|
||||||
|
{totalWeightAlerts ? (
|
||||||
|
<button className="range-alert-button" onClick={handleWeightRangeAlertClick} type="button">
|
||||||
|
Review weights
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{vetVisitDueBirds.length ? (
|
||||||
|
<button className="range-alert-button" onClick={handleVetVisitReminderClick} type="button">
|
||||||
|
Review vet visits
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{activePage === 'overview' ? (
|
{activePage === 'overview' ? (
|
||||||
<section className="stack-grid">
|
<section className="stack-grid">
|
||||||
@@ -3780,11 +3974,6 @@ function App() {
|
|||||||
<h2>30-day flock weight snapshot</h2>
|
<h2>30-day flock weight snapshot</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row overview-alert-actions">
|
<div className="button-row overview-alert-actions">
|
||||||
{totalWeightAlerts ? (
|
|
||||||
<button className="range-alert-button" onClick={handleWeightRangeAlertClick} type="button">
|
|
||||||
{totalWeightAlerts} weight alert{totalWeightAlerts === 1 ? '' : 's'}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
{birdsWithRecentWeights.length} current
|
{birdsWithRecentWeights.length} current
|
||||||
{overviewHistoricalSeriesCount > 0 ? `, ${overviewHistoricalSeriesCount} previous-year` : ''}
|
{overviewHistoricalSeriesCount > 0 ? `, ${overviewHistoricalSeriesCount} previous-year` : ''}
|
||||||
@@ -3909,43 +4098,12 @@ function App() {
|
|||||||
<strong>{missingFirstWeightCount}</strong>
|
<strong>{missingFirstWeightCount}</strong>
|
||||||
<span>Members still needing a first weight</span>
|
<span>Members still needing a first weight</span>
|
||||||
</article>
|
</article>
|
||||||
) : null}
|
) : (
|
||||||
{totalWeightAlerts ? (
|
<article className="summary-card">
|
||||||
<article className="summary-card summary-alert-card">
|
<span>First weights</span>
|
||||||
<span>Weight alerts</span>
|
<strong>All recorded</strong>
|
||||||
<strong>
|
|
||||||
{totalWeightAlerts} alert{totalWeightAlerts === 1 ? '' : 's'} need review
|
|
||||||
</strong>
|
|
||||||
{outOfRangeBirds.length ? (
|
|
||||||
<span>
|
|
||||||
{outOfRangeBirds.length} bird{outOfRangeBirds.length === 1 ? '' : 's'} outside typical ranges
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{weightDropAlerts.length ? (
|
|
||||||
<span>
|
|
||||||
{weightDropAlerts.length} bird{weightDropAlerts.length === 1 ? '' : 's'} down 5-10% between recent entries
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<button className="range-alert-button" onClick={handleWeightRangeAlertClick} type="button">
|
|
||||||
Review alerts
|
|
||||||
</button>
|
|
||||||
</article>
|
</article>
|
||||||
) : null}
|
)}
|
||||||
{vetVisitDueBirds.length ? (
|
|
||||||
<article className="summary-card summary-alert-card">
|
|
||||||
<span>Vet visit reminder</span>
|
|
||||||
<strong>
|
|
||||||
{vetVisitDueBirds.length} member{vetVisitDueBirds.length === 1 ? '' : 's'} need annual visit review
|
|
||||||
</strong>
|
|
||||||
<span>
|
|
||||||
No vet visit logged in the last 365 days for {vetVisitDueNames}
|
|
||||||
{vetVisitDueOverflowCount ? ` and ${vetVisitDueOverflowCount} more` : ''}.
|
|
||||||
</span>
|
|
||||||
<button className="range-alert-button" onClick={handleVetVisitReminderClick} type="button">
|
|
||||||
Review vet visits
|
|
||||||
</button>
|
|
||||||
</article>
|
|
||||||
) : null}
|
|
||||||
<article className="summary-card">
|
<article className="summary-card">
|
||||||
<span>Weekly flock changes</span>
|
<span>Weekly flock changes</span>
|
||||||
{flockWeeklyTrendItems.length ? (
|
{flockWeeklyTrendItems.length ? (
|
||||||
@@ -4247,8 +4405,14 @@ function App() {
|
|||||||
<>
|
<>
|
||||||
<section className="profile-hero">
|
<section className="profile-hero">
|
||||||
<img className="profile-photo" src={selectedBird.photoDataUrl || defaultBirdPhoto} alt={`${selectedBird.name}`} />
|
<img className="profile-photo" src={selectedBird.photoDataUrl || defaultBirdPhoto} alt={`${selectedBird.name}`} />
|
||||||
|
{selectedBird.publicProfileEnabled && selectedBird.publicProfileCode ? (
|
||||||
|
<button className="qr-profile-button" onClick={() => setQrBird(selectedBird)} type="button" aria-label={`Open QR code for ${selectedBird.name}`}>
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M3 3h7v7H3V3Zm2 2v3h3V5H5Zm9-2h7v7h-7V3Zm2 2v3h3V5h-3ZM3 14h7v7H3v-7Zm2 2v3h3v-3H5Zm10-1h2v2h-2v-2Zm4 0h2v2h-2v-2Zm-5 4h2v2h-2v-2Zm3-2h2v2h-2v-2Zm2 2h2v2h-2v-2Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
<div className="profile-copy">
|
<div className="profile-copy">
|
||||||
<p className="eyebrow">Profile</p>
|
|
||||||
<h3 className="profile-title">
|
<h3 className="profile-title">
|
||||||
<span>{selectedBird.name}</span>
|
<span>{selectedBird.name}</span>
|
||||||
<span
|
<span
|
||||||
@@ -4266,10 +4430,6 @@ function App() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="detail-grid">
|
<div className="detail-grid">
|
||||||
<article className="detail-card">
|
|
||||||
<span>Band ID</span>
|
|
||||||
<strong>{selectedBird.tagId || 'Not recorded'}</strong>
|
|
||||||
</article>
|
|
||||||
<article className="detail-card">
|
<article className="detail-card">
|
||||||
<span>Hatch Day</span>
|
<span>Hatch Day</span>
|
||||||
<strong>{formatDate(selectedBird.dateOfBirth)}</strong>
|
<strong>{formatDate(selectedBird.dateOfBirth)}</strong>
|
||||||
@@ -4278,10 +4438,6 @@ function App() {
|
|||||||
<span>Gotcha day</span>
|
<span>Gotcha day</span>
|
||||||
<strong>{formatDate(selectedBird.gotchaDay)}</strong>
|
<strong>{formatDate(selectedBird.gotchaDay)}</strong>
|
||||||
</article>
|
</article>
|
||||||
<article className="detail-card">
|
|
||||||
<span>Species</span>
|
|
||||||
<strong>{selectedBird.species}</strong>
|
|
||||||
</article>
|
|
||||||
<article className="detail-card">
|
<article className="detail-card">
|
||||||
<span>Gender</span>
|
<span>Gender</span>
|
||||||
<strong className="detail-gender">
|
<strong className="detail-gender">
|
||||||
@@ -5333,6 +5489,14 @@ function App() {
|
|||||||
placeholder="Optional if unknown"
|
placeholder="Optional if unknown"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="toggle-field">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={birdForm.publicProfileEnabled}
|
||||||
|
onChange={(event) => setBirdForm({ ...birdForm, publicProfileEnabled: event.target.checked })}
|
||||||
|
/>
|
||||||
|
<span>Enable QR public profile</span>
|
||||||
|
</label>
|
||||||
<label className="species-picker-field wide-field">
|
<label className="species-picker-field wide-field">
|
||||||
Species
|
Species
|
||||||
<div className="species-picker">
|
<div className="species-picker">
|
||||||
@@ -5897,6 +6061,38 @@ function App() {
|
|||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{qrBird?.publicProfileCode ? (
|
||||||
|
<div className="app-modal-backdrop" role="presentation" onClick={() => setQrBird(null)}>
|
||||||
|
<section
|
||||||
|
className="app-modal qr-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="qr-modal-title"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="panel-header no-print">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">QR profile</p>
|
||||||
|
<h2 id="qr-modal-title">{qrBird.name}</h2>
|
||||||
|
</div>
|
||||||
|
<div className="button-row">
|
||||||
|
<button className="secondary-button" onClick={() => window.print()} type="button">
|
||||||
|
Print
|
||||||
|
</button>
|
||||||
|
<button className="secondary-button" onClick={() => setQrBird(null)} type="button">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="qr-print-card">
|
||||||
|
<QrCodeWithLogo value={getPublicProfileUrl(qrBird.publicProfileCode)} label={`QR code for ${qrBird.name}`} />
|
||||||
|
<h3>{qrBird.name}</h3>
|
||||||
|
<p>{getPublicProfileUrl(qrBird.publicProfileCode)}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{showWeightAlertModal ? (
|
{showWeightAlertModal ? (
|
||||||
<div className="app-modal-backdrop" role="presentation" onClick={() => setShowWeightAlertModal(false)}>
|
<div className="app-modal-backdrop" role="presentation" onClick={() => setShowWeightAlertModal(false)}>
|
||||||
<section
|
<section
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
+273
-2
@@ -122,6 +122,70 @@ textarea {
|
|||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-alert-notification {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.9rem;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
border: 1px solid rgba(203, 58, 53, 0.26);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 247, 244, 0.98), rgba(255, 238, 231, 0.96));
|
||||||
|
box-shadow: 0 16px 30px rgba(203, 58, 53, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-alert-notification div {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-alert-notification strong {
|
||||||
|
color: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-alert-notification span {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(203, 58, 53, 0.12);
|
||||||
|
border: 1px solid rgba(203, 58, 53, 0.22);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 7px;
|
||||||
|
width: 12px;
|
||||||
|
height: 15px;
|
||||||
|
border: 2px solid var(--accent-red);
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: 8px 8px 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 22px;
|
||||||
|
width: 10px;
|
||||||
|
height: 5px;
|
||||||
|
border-top: 2px solid var(--accent-red);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-alert-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: end;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.side-rail {
|
.side-rail {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 2rem;
|
top: 2rem;
|
||||||
@@ -155,6 +219,41 @@ textarea {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.public-profile-shell {
|
||||||
|
max-width: 620px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-profile-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.1rem;
|
||||||
|
justify-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-profile-photo {
|
||||||
|
width: min(260px, 100%);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 28px;
|
||||||
|
border: 1px solid rgba(39, 105, 179, 0.16);
|
||||||
|
background: rgba(255, 255, 255, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-profile-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-profile-copy h1 {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-hero-card {
|
.auth-hero-card {
|
||||||
min-height: 280px;
|
min-height: 280px;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
@@ -948,6 +1047,32 @@ textarea {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.92), rgba(240, 248, 244, 0.84));
|
background: linear-gradient(180deg, rgba(255, 252, 247, 0.92), rgba(240, 248, 244, 0.84));
|
||||||
border: 1px solid rgba(39, 105, 179, 0.1);
|
border: 1px solid rgba(39, 105, 179, 0.1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-profile-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.85rem;
|
||||||
|
right: 0.85rem;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border: 1px solid rgba(39, 105, 179, 0.18);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 254, 250, 0.9);
|
||||||
|
box-shadow: 0 10px 20px rgba(86, 63, 34, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-profile-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: rgba(35, 138, 90, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-profile-button svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
fill: var(--accent-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-copy {
|
.profile-copy {
|
||||||
@@ -1371,6 +1496,21 @@ label {
|
|||||||
accent-color: var(--accent-green);
|
accent-color: var(--accent-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
padding-top: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-field input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
accent-color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
.primary-button {
|
.primary-button {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
@@ -1555,11 +1695,79 @@ label {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qr-modal {
|
||||||
|
width: min(520px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-print-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.8rem;
|
||||||
|
justify-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: #fffdf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
width: min(280px, 100%);
|
||||||
|
height: auto;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-bird-mark rect {
|
||||||
|
fill: rgba(255, 255, 255, 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-print-card h3,
|
||||||
|
.qr-print-card p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-print-card p {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-alert-list {
|
.modal-alert-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.9rem;
|
gap: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before,
|
||||||
|
.no-print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-modal-backdrop {
|
||||||
|
position: static;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-modal {
|
||||||
|
box-shadow: none;
|
||||||
|
border: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-print-card {
|
||||||
|
min-height: 100vh;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.app-shell,
|
.app-shell,
|
||||||
.auth-panel,
|
.auth-panel,
|
||||||
@@ -1577,6 +1785,7 @@ label {
|
|||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
gap: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-grid {
|
.settings-grid {
|
||||||
@@ -1588,11 +1797,73 @@ label {
|
|||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-nav {
|
.top-alert-notification {
|
||||||
position: static;
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-alert-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-rail {
|
.side-rail {
|
||||||
position: static;
|
position: static;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-lockup {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav.panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
padding: 0.65rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-tabs {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(82px, 1fr));
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-tab {
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 0.55rem 0.65rem;
|
||||||
|
border-radius: 14px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav .secondary-button {
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
border-radius: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-switcher {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-switcher-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.45rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-switcher-item {
|
||||||
|
min-width: 160px;
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-switcher-item small {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user