visual improvements
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node --import tsx src/app.ts",
|
"dev": "tsx watch src/app.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/app.js"
|
"start": "node dist/app.js"
|
||||||
},
|
},
|
||||||
|
|||||||
+100
-23
@@ -39,6 +39,11 @@ const allowedOrigins = Array.from(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const dateStringSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
const dateStringSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
||||||
|
const chartColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/);
|
||||||
|
const photoDataUrlSchema = z
|
||||||
|
.string()
|
||||||
|
.regex(/^data:image\/(?:png|jpeg|jpg|webp|gif);base64,[A-Za-z0-9+/=]+$/)
|
||||||
|
.max(1_500_000);
|
||||||
|
|
||||||
const birdSchema = z.object({
|
const birdSchema = z.object({
|
||||||
name: z.string().trim().min(1).max(120),
|
name: z.string().trim().min(1).max(120),
|
||||||
@@ -46,6 +51,8 @@ const birdSchema = z.object({
|
|||||||
species: z.string().trim().min(1).max(120),
|
species: z.string().trim().min(1).max(120),
|
||||||
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
|
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
|
||||||
gotchaDay: dateStringSchema.optional().or(z.literal('')),
|
gotchaDay: dateStringSchema.optional().or(z.literal('')),
|
||||||
|
chartColor: chartColorSchema.optional(),
|
||||||
|
photoDataUrl: photoDataUrlSchema.optional().or(z.literal('')),
|
||||||
});
|
});
|
||||||
|
|
||||||
const weightSchema = z.object({
|
const weightSchema = z.object({
|
||||||
@@ -68,6 +75,8 @@ type BirdRow = {
|
|||||||
species: string;
|
species: string;
|
||||||
date_of_birth: string | null;
|
date_of_birth: string | null;
|
||||||
gotcha_day: string | null;
|
gotcha_day: string | null;
|
||||||
|
chart_color: string;
|
||||||
|
photo_data_url: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
latest_weight_grams: string | null;
|
latest_weight_grams: string | null;
|
||||||
latest_recorded_on: string | null;
|
latest_recorded_on: string | null;
|
||||||
@@ -111,7 +120,7 @@ app.use(
|
|||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use(express.json({ limit: '300kb' }));
|
app.use(express.json({ limit: '2mb' }));
|
||||||
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
||||||
|
|
||||||
const emptyToNull = (value?: string) => {
|
const emptyToNull = (value?: string) => {
|
||||||
@@ -126,11 +135,27 @@ const normalizeBird = (row: BirdRow) => ({
|
|||||||
species: row.species,
|
species: row.species,
|
||||||
dateOfBirth: row.date_of_birth,
|
dateOfBirth: row.date_of_birth,
|
||||||
gotchaDay: row.gotcha_day,
|
gotchaDay: row.gotcha_day,
|
||||||
|
chartColor: row.chart_color,
|
||||||
|
photoDataUrl: row.photo_data_url,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
latestWeightGrams: row.latest_weight_grams ? Number(row.latest_weight_grams) : null,
|
latestWeightGrams: row.latest_weight_grams ? Number(row.latest_weight_grams) : null,
|
||||||
latestRecordedOn: row.latest_recorded_on,
|
latestRecordedOn: row.latest_recorded_on,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const birdSelectFields = `
|
||||||
|
birds.id,
|
||||||
|
birds.name,
|
||||||
|
birds.tag_id,
|
||||||
|
birds.species,
|
||||||
|
birds.date_of_birth::text,
|
||||||
|
birds.gotcha_day::text,
|
||||||
|
birds.chart_color,
|
||||||
|
birds.photo_data_url,
|
||||||
|
birds.created_at,
|
||||||
|
latest.weight_grams AS latest_weight_grams,
|
||||||
|
latest.recorded_on::text AS latest_recorded_on
|
||||||
|
`;
|
||||||
|
|
||||||
const normalizeWeight = (row: WeightRow) => ({
|
const normalizeWeight = (row: WeightRow) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
birdId: row.bird_id,
|
birdId: row.bird_id,
|
||||||
@@ -159,12 +184,16 @@ const ensureSchema = async () => {
|
|||||||
species VARCHAR(120) NOT NULL,
|
species VARCHAR(120) NOT NULL,
|
||||||
date_of_birth DATE,
|
date_of_birth DATE,
|
||||||
gotcha_day DATE,
|
gotcha_day DATE,
|
||||||
|
chart_color VARCHAR(7) NOT NULL DEFAULT '#cb3a35',
|
||||||
|
photo_data_url TEXT,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE birds
|
ALTER TABLE birds
|
||||||
ADD COLUMN IF NOT EXISTS date_of_birth DATE,
|
ADD COLUMN IF NOT EXISTS date_of_birth DATE,
|
||||||
ADD COLUMN IF NOT EXISTS gotcha_day DATE;
|
ADD COLUMN IF NOT EXISTS gotcha_day DATE,
|
||||||
|
ADD COLUMN IF NOT EXISTS chart_color VARCHAR(7) NOT NULL DEFAULT '#cb3a35',
|
||||||
|
ADD COLUMN IF NOT EXISTS photo_data_url TEXT;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS weight_records (
|
CREATE TABLE IF NOT EXISTS weight_records (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
@@ -197,15 +226,7 @@ const ensureSchema = async () => {
|
|||||||
const getBirdById = async (birdId: string) => {
|
const getBirdById = async (birdId: string) => {
|
||||||
const result = await pool.query<BirdRow>(
|
const result = await pool.query<BirdRow>(
|
||||||
`SELECT
|
`SELECT
|
||||||
birds.id,
|
${birdSelectFields}
|
||||||
birds.name,
|
|
||||||
birds.tag_id,
|
|
||||||
birds.species,
|
|
||||||
birds.date_of_birth::text,
|
|
||||||
birds.gotcha_day::text,
|
|
||||||
birds.created_at,
|
|
||||||
latest.weight_grams AS latest_weight_grams,
|
|
||||||
latest.recorded_on::text AS latest_recorded_on
|
|
||||||
FROM birds
|
FROM birds
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT weight_grams, recorded_on
|
SELECT weight_grams, recorded_on
|
||||||
@@ -229,15 +250,7 @@ app.get('/api/birds', async (_req: Request, res: Response, next: NextFunction) =
|
|||||||
try {
|
try {
|
||||||
const result = await pool.query<BirdRow>(`
|
const result = await pool.query<BirdRow>(`
|
||||||
SELECT
|
SELECT
|
||||||
birds.id,
|
${birdSelectFields}
|
||||||
birds.name,
|
|
||||||
birds.tag_id,
|
|
||||||
birds.species,
|
|
||||||
birds.date_of_birth::text,
|
|
||||||
birds.gotcha_day::text,
|
|
||||||
birds.created_at,
|
|
||||||
latest.weight_grams AS latest_weight_grams,
|
|
||||||
latest.recorded_on::text AS latest_recorded_on
|
|
||||||
FROM birds
|
FROM birds
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT weight_grams, recorded_on
|
SELECT weight_grams, recorded_on
|
||||||
@@ -265,15 +278,17 @@ app.post('/api/birds', async (req: Request, res: Response, next: NextFunction) =
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await pool.query<BirdRow>(
|
const result = await pool.query<BirdRow>(
|
||||||
`INSERT INTO birds (name, tag_id, species, date_of_birth, gotcha_day)
|
`INSERT INTO birds (name, tag_id, species, date_of_birth, gotcha_day, chart_color, photo_data_url)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING id, name, tag_id, species, date_of_birth::text, gotcha_day::text, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
|
RETURNING id, name, tag_id, species, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
|
||||||
[
|
[
|
||||||
parsed.data.name,
|
parsed.data.name,
|
||||||
parsed.data.tagId,
|
parsed.data.tagId,
|
||||||
parsed.data.species,
|
parsed.data.species,
|
||||||
emptyToNull(parsed.data.dateOfBirth),
|
emptyToNull(parsed.data.dateOfBirth),
|
||||||
emptyToNull(parsed.data.gotchaDay),
|
emptyToNull(parsed.data.gotchaDay),
|
||||||
|
parsed.data.chartColor ?? '#cb3a35',
|
||||||
|
emptyToNull(parsed.data.photoDataUrl),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -288,6 +303,68 @@ app.post('/api/birds', async (req: Request, res: Response, next: NextFunction) =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.put('/api/birds/:birdId', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const parsed = birdSchema.safeParse(req.body);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ error: 'Invalid bird payload', details: parsed.error.flatten() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pool.query<BirdRow>(
|
||||||
|
`UPDATE birds
|
||||||
|
SET name = $2,
|
||||||
|
tag_id = $3,
|
||||||
|
species = $4,
|
||||||
|
date_of_birth = $5,
|
||||||
|
gotcha_day = $6,
|
||||||
|
chart_color = $7,
|
||||||
|
photo_data_url = $8
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, name, tag_id, species, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, created_at,
|
||||||
|
(
|
||||||
|
SELECT weight_grams::text
|
||||||
|
FROM weight_records
|
||||||
|
WHERE bird_id = birds.id
|
||||||
|
ORDER BY recorded_on DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS latest_weight_grams,
|
||||||
|
(
|
||||||
|
SELECT recorded_on::text
|
||||||
|
FROM weight_records
|
||||||
|
WHERE bird_id = birds.id
|
||||||
|
ORDER BY recorded_on DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS latest_recorded_on`,
|
||||||
|
[
|
||||||
|
req.params.birdId,
|
||||||
|
parsed.data.name,
|
||||||
|
parsed.data.tagId,
|
||||||
|
parsed.data.species,
|
||||||
|
emptyToNull(parsed.data.dateOfBirth),
|
||||||
|
emptyToNull(parsed.data.gotchaDay),
|
||||||
|
parsed.data.chartColor ?? '#cb3a35',
|
||||||
|
emptyToNull(parsed.data.photoDataUrl),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.rowCount) {
|
||||||
|
res.status(404).json({ error: 'Bird not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ bird: normalizeBird(result.rows[0]) });
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
||||||
|
res.status(409).json({ error: 'That tag ID is already in use.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.delete('/api/birds/:birdId', async (req: Request, res: Response, next: NextFunction) => {
|
app.delete('/api/birds/:birdId', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const result = await pool.query<{ id: string }>('DELETE FROM birds WHERE id = $1 RETURNING id', [req.params.birdId]);
|
const result = await pool.query<{ id: string }>('DELETE FROM birds WHERE id = $1 RETURNING id', [req.params.birdId]);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ RUN npm install
|
|||||||
COPY tsconfig*.json ./
|
COPY tsconfig*.json ./
|
||||||
COPY vite.config.ts ./
|
COPY vite.config.ts ./
|
||||||
COPY index.html ./
|
COPY index.html ./
|
||||||
COPY public ./public
|
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["npm", "run", "dev", "--", "--host"]
|
CMD ["npm", "run", "dev", "--", "--host"]
|
||||||
|
|||||||
+656
-300
File diff suppressed because it is too large
Load Diff
+204
-45
@@ -11,10 +11,12 @@
|
|||||||
--accent-teal: #2f8f98;
|
--accent-teal: #2f8f98;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top left, rgba(203, 58, 53, 0.24), transparent 24%),
|
radial-gradient(circle at 14% 10%, rgba(222, 124, 58, 0.28), transparent 22%),
|
||||||
radial-gradient(circle at 88% 22%, rgba(39, 105, 179, 0.22), transparent 20%),
|
radial-gradient(circle at 82% 12%, rgba(53, 136, 110, 0.26), transparent 20%),
|
||||||
radial-gradient(circle at bottom right, rgba(35, 138, 90, 0.24), transparent 28%),
|
radial-gradient(circle at 24% 84%, rgba(221, 179, 78, 0.2), transparent 22%),
|
||||||
linear-gradient(180deg, #fff3e8 0%, #f6ead7 55%, #eef7f2 100%);
|
radial-gradient(circle at 86% 78%, rgba(43, 118, 92, 0.24), transparent 24%),
|
||||||
|
radial-gradient(circle at 62% 54%, rgba(48, 114, 160, 0.14), transparent 16%),
|
||||||
|
linear-gradient(180deg, #fef5e7 0%, #e9ddba 46%, #d9eadf 100%);
|
||||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -34,11 +36,27 @@ body,
|
|||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.32;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 720 720'%3E%3Crect width='720' height='720' fill='none'/%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3Cg stroke='%236db7b4' transform='translate(72 88) rotate(-18)'%3E%3Cpath d='M0 0 L-24 -42'/%3E%3Cpath d='M0 0 L12 -50'/%3E%3Cpath d='M0 0 L44 -18'/%3E%3C/g%3E%3Cg stroke='%2396c489' transform='translate(248 70) rotate(14)'%3E%3Cpath d='M0 0 L-28 -38'/%3E%3Cpath d='M0 0 L4 -52'/%3E%3Cpath d='M0 0 L40 -30'/%3E%3C/g%3E%3Cg stroke='%23f5a347' transform='translate(430 92) rotate(-10)'%3E%3Cpath d='M0 0 L-30 -32'/%3E%3Cpath d='M0 0 L10 -50'/%3E%3Cpath d='M0 0 L46 -14'/%3E%3C/g%3E%3Cg stroke='%23888690' transform='translate(604 64) rotate(20)'%3E%3Cpath d='M0 0 L-26 -36'/%3E%3Cpath d='M0 0 L2 -54'/%3E%3Cpath d='M0 0 L36 -34'/%3E%3C/g%3E%3Cg stroke='%23b5c948' transform='translate(126 220) rotate(8)'%3E%3Cpath d='M0 0 L-22 -46'/%3E%3Cpath d='M0 0 L6 -54'/%3E%3Cpath d='M0 0 L42 -26'/%3E%3C/g%3E%3Cg stroke='%236db7b4' transform='translate(330 246) rotate(-24)'%3E%3Cpath d='M0 0 L-28 -34'/%3E%3Cpath d='M0 0 L0 -56'/%3E%3Cpath d='M0 0 L38 -30'/%3E%3C/g%3E%3Cg stroke='%23888690' transform='translate(520 224) rotate(18)'%3E%3Cpath d='M0 0 L-20 -46'/%3E%3Cpath d='M0 0 L6 -52'/%3E%3Cpath d='M0 0 L34 -36'/%3E%3C/g%3E%3Cg stroke='%2396c489' transform='translate(654 220) rotate(-14)'%3E%3Cpath d='M0 0 L-34 -26'/%3E%3Cpath d='M0 0 L-4 -54'/%3E%3Cpath d='M0 0 L34 -38'/%3E%3C/g%3E%3Cg stroke='%23f5a347' transform='translate(62 402) rotate(22)'%3E%3Cpath d='M0 0 L-30 -28'/%3E%3Cpath d='M0 0 L2 -54'/%3E%3Cpath d='M0 0 L38 -26'/%3E%3C/g%3E%3Cg stroke='%23888690' transform='translate(250 386) rotate(-18)'%3E%3Cpath d='M0 0 L-24 -42'/%3E%3Cpath d='M0 0 L10 -50'/%3E%3Cpath d='M0 0 L40 -22'/%3E%3C/g%3E%3Cg stroke='%2396c489' transform='translate(438 410) rotate(10)'%3E%3Cpath d='M0 0 L-28 -38'/%3E%3Cpath d='M0 0 L4 -54'/%3E%3Cpath d='M0 0 L44 -24'/%3E%3C/g%3E%3Cg stroke='%236db7b4' transform='translate(610 392) rotate(-20)'%3E%3Cpath d='M0 0 L-26 -34'/%3E%3Cpath d='M0 0 L8 -54'/%3E%3Cpath d='M0 0 L40 -18'/%3E%3C/g%3E%3Cg stroke='%23b5c948' transform='translate(122 564) rotate(16)'%3E%3Cpath d='M0 0 L-30 -30'/%3E%3Cpath d='M0 0 L2 -56'/%3E%3Cpath d='M0 0 L44 -20'/%3E%3C/g%3E%3Cg stroke='%23888690' transform='translate(330 576) rotate(-12)'%3E%3Cpath d='M0 0 L-26 -40'/%3E%3Cpath d='M0 0 L10 -52'/%3E%3Cpath d='M0 0 L40 -28'/%3E%3C/g%3E%3Cg stroke='%23f5a347' transform='translate(520 566) rotate(24)'%3E%3Cpath d='M0 0 L-28 -34'/%3E%3Cpath d='M0 0 L2 -54'/%3E%3Cpath d='M0 0 L42 -24'/%3E%3C/g%3E%3Cg stroke='%2396c489' transform='translate(678 586) rotate(-18)'%3E%3Cpath d='M0 0 L-26 -38'/%3E%3Cpath d='M0 0 L6 -54'/%3E%3Cpath d='M0 0 L36 -28'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 360px 360px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
textarea {
|
textarea,
|
||||||
|
select {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +69,8 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
textarea {
|
textarea,
|
||||||
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
border: 1px solid rgba(39, 105, 179, 0.16);
|
border: 1px solid rgba(39, 105, 179, 0.16);
|
||||||
@@ -62,7 +81,8 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input:focus,
|
input:focus,
|
||||||
textarea:focus {
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: rgba(39, 105, 179, 0.62);
|
border-color: rgba(39, 105, 179, 0.62);
|
||||||
box-shadow: 0 0 0 4px rgba(39, 105, 179, 0.14);
|
box-shadow: 0 0 0 4px rgba(39, 105, 179, 0.14);
|
||||||
@@ -73,11 +93,25 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
max-width: 1280px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: 240px minmax(0, 1fr);
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-shell {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 2rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stack-grid {
|
.stack-grid {
|
||||||
@@ -107,22 +141,6 @@ textarea {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-card::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: auto -8% -42% auto;
|
|
||||||
width: 280px;
|
|
||||||
height: 280px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 35% 35%, rgba(240, 182, 63, 0.62), transparent 26%),
|
|
||||||
radial-gradient(circle at 58% 44%, rgba(35, 138, 90, 0.52), transparent 32%),
|
|
||||||
radial-gradient(circle at 72% 62%, rgba(39, 105, 179, 0.5), transparent 30%),
|
|
||||||
radial-gradient(circle at 42% 74%, rgba(203, 58, 53, 0.52), transparent 32%);
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-card h1,
|
.hero-card h1,
|
||||||
.panel h2 {
|
.panel h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -135,21 +153,18 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-tabs {
|
.page-tabs {
|
||||||
display: flex;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-tab {
|
.page-tab {
|
||||||
border: 1px solid rgba(39, 105, 179, 0.14);
|
border: 1px solid rgba(39, 105, 179, 0.14);
|
||||||
border-radius: 999px;
|
border-radius: 18px;
|
||||||
padding: 0.7rem 1.1rem;
|
padding: 0.95rem 1rem;
|
||||||
background: rgba(255, 255, 255, 0.54);
|
background: rgba(255, 255, 255, 0.54);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease;
|
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-tab.active {
|
.page-tab.active {
|
||||||
@@ -265,11 +280,49 @@ textarea {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.bird-list {
|
.bird-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.9rem;
|
gap: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bird-card-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 0.85rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-avatar,
|
||||||
|
.profile-photo {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(39, 105, 179, 0.16);
|
||||||
|
background: rgba(255, 255, 255, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-photo {
|
||||||
|
width: 112px;
|
||||||
|
height: 112px;
|
||||||
|
border-radius: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-avatar {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: linear-gradient(135deg, rgba(203, 58, 53, 0.14), rgba(39, 105, 179, 0.18));
|
||||||
|
color: var(--accent-red);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.bird-card {
|
.bird-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -311,17 +364,8 @@ textarea {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-card::after {
|
.overview-chart-card::before {
|
||||||
content: "";
|
display: none;
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
width: 110px;
|
|
||||||
height: 110px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle, rgba(39, 105, 179, 0.14), transparent 58%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-chart-card {
|
.overview-chart-card {
|
||||||
@@ -334,6 +378,22 @@ textarea {
|
|||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-grid-line {
|
||||||
|
stroke: rgba(39, 105, 179, 0.16);
|
||||||
|
stroke-width: 1;
|
||||||
|
stroke-dasharray: 4 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-axis-line {
|
||||||
|
stroke: rgba(31, 42, 42, 0.24);
|
||||||
|
stroke-width: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-axis-label {
|
||||||
|
fill: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-footer,
|
.chart-footer,
|
||||||
.recent-list,
|
.recent-list,
|
||||||
.detail-grid,
|
.detail-grid,
|
||||||
@@ -361,6 +421,27 @@ textarea {
|
|||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 24px;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-copy h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-form {
|
.inline-form {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@@ -399,6 +480,11 @@ textarea {
|
|||||||
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.65);
|
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.large-swatch {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-card span,
|
.detail-card span,
|
||||||
.summary-card span {
|
.summary-card span {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -443,6 +529,19 @@ label {
|
|||||||
background: linear-gradient(135deg, #b7312d, #1f5e9f);
|
background: linear-gradient(135deg, #b7312d, #1f5e9f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secondary-button {
|
||||||
|
border: 1px solid rgba(39, 105, 179, 0.18);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 0.95rem 1.2rem;
|
||||||
|
color: var(--ink);
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
box-shadow: 0 10px 22px rgba(39, 105, 179, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
.danger-button {
|
.danger-button {
|
||||||
border: 1px solid rgba(171, 44, 44, 0.18);
|
border: 1px solid rgba(171, 44, 44, 0.18);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
@@ -470,13 +569,73 @@ label {
|
|||||||
color: #922728;
|
color: #922728;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.picker-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-preview-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.9rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.95rem 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 252, 247, 0.9), rgba(240, 248, 244, 0.82));
|
||||||
|
border: 1px solid rgba(39, 105, 179, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-chip {
|
||||||
|
border: 1px solid rgba(39, 105, 179, 0.16);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.65rem 0.95rem;
|
||||||
|
background: rgba(255, 255, 255, 0.68);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-chip.active {
|
||||||
|
border-color: transparent;
|
||||||
|
background: linear-gradient(135deg, rgba(203, 58, 53, 0.94), rgba(39, 105, 179, 0.94));
|
||||||
|
color: #fffdf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-editor {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 252, 247, 0.9), rgba(240, 248, 244, 0.82));
|
||||||
|
border: 1px solid rgba(39, 105, 179, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-preview-shell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-picker input[type="file"] {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
|
.app-shell,
|
||||||
.hero-card,
|
.hero-card,
|
||||||
.dashboard-grid,
|
.dashboard-grid,
|
||||||
.forms-grid,
|
.forms-grid,
|
||||||
.hero-stats,
|
.hero-stats,
|
||||||
.chart-footer,
|
.chart-footer,
|
||||||
.inline-form {
|
.inline-form,
|
||||||
|
.profile-hero,
|
||||||
|
.photo-editor {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +643,7 @@ label {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-tabs {
|
.side-nav {
|
||||||
margin-top: 1rem;
|
position: static;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user