mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
commit
e4fb19c245
13 changed files with 283 additions and 90 deletions
|
|
@ -13,3 +13,5 @@ export const filesFromSurveyor = pgTable("files_from_surveyor", {
|
|||
s3JsonUrl: text("s3_json_url").notNull(),
|
||||
uploadedAt: timestamp("uploaded_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type FilesFromSurveyor = typeof filesFromSurveyor.$inferSelect;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export async function MagicLinksEmail({
|
|||
const result = await transport.sendMail({
|
||||
to: identifier,
|
||||
from: provider.from,
|
||||
subject: "Your secure Domna IQ sign-in link",
|
||||
subject: "Your secure Ara sign-in link",
|
||||
text: plainText({ url, host }),
|
||||
html: domnaHtml({ url, host, brandColor, accentColor, brown, background }),
|
||||
});
|
||||
|
|
@ -70,13 +70,13 @@ function domnaHtml({
|
|||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 10px 10px; color: #333;">
|
||||
<h2 style="color: ${brandColor}; font-size: 22px; margin-bottom: 16px;">Welcome back to Domna IQ</h2>
|
||||
<h2 style="color: ${brandColor}; font-size: 22px; margin-bottom: 16px;">Welcome back to Ara by Domna</h2>
|
||||
<p style="font-size: 16px; line-height: 1.6; color: #444; margin-bottom: 32px;">
|
||||
Click below to securely sign in to your account and continue your retrofit journey.
|
||||
</p>
|
||||
<a href="${url}" target="_blank"
|
||||
style="display: inline-block; padding: 14px 28px; background: ${brown}; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 16px;">
|
||||
Sign in to Domna IQ
|
||||
Sign in to Ara
|
||||
</a>
|
||||
<p style="margin-top: 36px; font-size: 13px; color: #777;">
|
||||
If you didn’t request this email, you can safely ignore it.
|
||||
|
|
@ -94,5 +94,5 @@ function domnaHtml({
|
|||
}
|
||||
|
||||
function plainText({ url, host }: { url: string; host: string }) {
|
||||
return `Sign in to Domna IQ\n${url}\n\nIf you did not request this email, you can safely ignore it.\n`;
|
||||
return `Sign in to Ara by Domna\n${url}\n\nIf you did not request this email, you can safely ignore it.\n`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export default function OnboardingPage() {
|
|||
|
||||
<div className="relative z-10 flex items-start justify-start w-full h-full">
|
||||
<div className="mt-20 p-20 text-gray-100 shadow-xl w-[75%] rounded-br-[8rem] bg-gradient-to-r from-brandblue to-midblue">
|
||||
<h2 className="text-5xl font-bold mb-4">Welcome to Domna IQ</h2>
|
||||
<h2 className="text-5xl font-bold mb-4">Welcome to Ara</h2>
|
||||
<p className="text-xl leading-relaxed text-brandbrown">
|
||||
Help us get to know you so we can tailor your experience.
|
||||
</p>
|
||||
|
|
|
|||
14
src/app/portfolio/[slug]/(portfolio)/reporting/page.tsx
Normal file
14
src/app/portfolio/[slug]/(portfolio)/reporting/page.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export default async function ReportingPage(props: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const portfolioId = params.slug;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
<div>Reporting Page for portfolio: {portfolioId}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
1
src/app/portfolio/[slug]/(portfolio)/reporting/utils.ts
Normal file
1
src/app/portfolio/[slug]/(portfolio)/reporting/utils.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -4,82 +4,186 @@ import React from "react";
|
|||
import { TableCell, TableRow } from "@/app/shadcn_components/ui/table";
|
||||
import { BrandButton } from "@/app/components/Buttons";
|
||||
import { UploadModal } from "./UploadModal";
|
||||
import { documentTypeTitles, type ReportType } from "@/app/db/surveyDB/schema/documents";
|
||||
import type { getUploadedFiles, getUploadedFile } from "@/app/db/surveyDB/schema/surveyDB";
|
||||
import {
|
||||
documentTypeTitles,
|
||||
type ReportType,
|
||||
} from "@/app/db/surveyDB/schema/documents";
|
||||
import type { getUploadedFiles } from "@/app/db/surveyDB/schema/surveyDB";
|
||||
import type { FilesFromSurveyor } from "@/app/db/schema/files_from_surveyor";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
type Props = {
|
||||
reportType: ReportType;
|
||||
reportType: ReportType | null;
|
||||
uprn: string;
|
||||
files: getUploadedFiles;
|
||||
files: getUploadedFiles | FilesFromSurveyor[];
|
||||
};
|
||||
|
||||
export const DocumentSection: React.FC<Props> = ({ reportType, uprn, files }) => {
|
||||
type AnyFile = Record<string, unknown>;
|
||||
|
||||
const DATE_KEYS = [
|
||||
"uploadedAt",
|
||||
"s3FileUploadTimestamp",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"timestamp",
|
||||
] as const;
|
||||
|
||||
const URL_KEYS = ["s3FileUri", "s3JsonUrl", "url", "href", "link"] as const;
|
||||
|
||||
function pickFirst<T = unknown>(
|
||||
obj: AnyFile,
|
||||
keys: readonly string[]
|
||||
): T | undefined {
|
||||
for (const k of keys) {
|
||||
if (k in obj && obj[k] != null) return obj[k] as T;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function asDate(val: unknown): Date | undefined {
|
||||
if (!val) return undefined;
|
||||
if (val instanceof Date) return val;
|
||||
const d = new Date(String(val));
|
||||
return isNaN(d.getTime()) ? undefined : d;
|
||||
}
|
||||
|
||||
type NormalizedFile = {
|
||||
url?: string;
|
||||
uploadedAt?: Date;
|
||||
original: AnyFile;
|
||||
};
|
||||
|
||||
const NAME_KEYS = ["name", "filename", "fileName", "title"] as const;
|
||||
|
||||
function normalizeFile(f: AnyFile): NormalizedFile & { name?: string } {
|
||||
const url = pickFirst<string>(f, URL_KEYS);
|
||||
const dateRaw = pickFirst<unknown>(f, DATE_KEYS);
|
||||
const uploadedAt = asDate(dateRaw);
|
||||
|
||||
let name = pickFirst<string>(f, NAME_KEYS);
|
||||
if (!name && url) {
|
||||
// Extract last path segment from URL
|
||||
const parts = url.split("/");
|
||||
name = decodeURIComponent(parts[parts.length - 1]);
|
||||
}
|
||||
|
||||
return { url, uploadedAt, name, original: f };
|
||||
}
|
||||
|
||||
function getLatestNormalized(files: AnyFile[]): NormalizedFile | null {
|
||||
if (!Array.isArray(files) || files.length === 0) return null;
|
||||
const normalized = files.map(normalizeFile);
|
||||
|
||||
// Prefer items that have a valid uploadedAt. If none have a date, fall back to first.
|
||||
const withDate = normalized.filter((n) => n.uploadedAt);
|
||||
if (withDate.length === 0) return normalized[0];
|
||||
|
||||
return withDate.reduce((acc, cur) =>
|
||||
cur.uploadedAt!.getTime() > acc.uploadedAt!.getTime() ? cur : acc
|
||||
);
|
||||
}
|
||||
|
||||
function formatWhen(d: Date | string) {
|
||||
const date = new Date(d);
|
||||
const yyyy = date.getUTCFullYear();
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
const dd = String(date.getUTCDate()).padStart(2, "0");
|
||||
const hh = String(date.getUTCHours()).padStart(2, "0");
|
||||
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
||||
return `${dd} ${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getUTCMonth()]} ${yyyy}, ${hh}:${min} UTC`;
|
||||
}
|
||||
|
||||
export function usePresignedUrl() {
|
||||
return useMutation({
|
||||
mutationFn: async (key: string) => {
|
||||
const res = await fetch("/api/sign-s3-url", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to get presigned URL");
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return data.url as string;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const DocumentSection: React.FC<Props> = ({
|
||||
reportType,
|
||||
uprn,
|
||||
files,
|
||||
}) => {
|
||||
const [showUploadModal, setShowUploadModal] = React.useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
console.log("files", files)
|
||||
const latest = React.useMemo(
|
||||
() => getLatestNormalized(files as unknown as AnyFile[]),
|
||||
[files]
|
||||
);
|
||||
|
||||
// const latestFile = React.useMemo<getUploadedFile | null>(() => {
|
||||
// if (!files?.length) return null;
|
||||
// return files.reduce((acc, cur) => {
|
||||
// const accTime = new Date(acc.s3FileUploadTimestamp as any).getTime();
|
||||
// const curTime = new Date(cur.s3FileUploadTimestamp as any).getTime();
|
||||
// return curTime > accTime ? cur : acc;
|
||||
// }, files[0]);
|
||||
// }, [files]);
|
||||
const title =
|
||||
reportType && documentTypeTitles[reportType] !== "Other"
|
||||
? documentTypeTitles[reportType]
|
||||
: latest?.url
|
||||
? decodeURIComponent(latest.url.split("/").pop() || "")
|
||||
: "Other";
|
||||
|
||||
// const latestFile = React.useMemo(() => {
|
||||
// console.log("Recomputing latestFile from", files);
|
||||
// if (!Array.isArray(files) || files.length === 0) return null;
|
||||
|
||||
// return files.reduce((acc, cur) => {
|
||||
// const accTime = new Date(acc.s3FileUploadTimestamp).getTime();
|
||||
// const curTime = new Date(cur.s3FileUploadTimestamp).getTime();
|
||||
// return curTime > accTime ? cur : acc;
|
||||
// }, files[0]);
|
||||
// }, [JSON.stringify(files)]);
|
||||
|
||||
const latestFile = files.length > 0 ? files.reduce((acc, cur) => {
|
||||
return new Date(cur.s3FileUploadTimestamp).getTime() > new Date(acc.s3FileUploadTimestamp).getTime() ? cur : acc;
|
||||
}) : null;
|
||||
|
||||
const formatWhen = (d: string | Date) =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
timeZone: "UTC"
|
||||
}).format(new Date(d));
|
||||
|
||||
const title = documentTypeTitles[reportType];
|
||||
const count = files.length;
|
||||
|
||||
const latestUpload = latestFile ? String(formatWhen(latestFile.s3FileUploadTimestamp)) : "";
|
||||
// console.log("latestFile", latestFile)
|
||||
|
||||
const latestUpload = latest?.uploadedAt ? formatWhen(latest.uploadedAt) : "";
|
||||
|
||||
const { mutateAsync: getSignedUrl, isPending } = usePresignedUrl();
|
||||
|
||||
const handleViewFile = async (fileUrl: string) => {
|
||||
try {
|
||||
// Extract S3 key from the full URL
|
||||
const bucketDomain = "retrofit-data-dev.s3.eu-west-2.amazonaws.com/";
|
||||
const key = fileUrl.split(bucketDomain)[1];
|
||||
if (!key) throw new Error("Invalid S3 file URL");
|
||||
|
||||
const signedUrl = await getSignedUrl(key);
|
||||
window.open(signedUrl, "_blank", "noopener,noreferrer");
|
||||
} catch (err) {
|
||||
console.error("Failed to open presigned URL:", err);
|
||||
alert("Could not open file.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableCell className="px-6 py-4 text-sm text-gray-900">
|
||||
<TableCell
|
||||
className="px-4 py-4 text-sm text-gray-900 max-w-[250px] truncate"
|
||||
title={title}
|
||||
>
|
||||
{title}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-6 py-4 text-sm text-gray-500">
|
||||
{latestFile ? (
|
||||
{latest ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<a
|
||||
href={latestFile.s3FileUri}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-2 hover:no-underline"
|
||||
>
|
||||
View latest file
|
||||
</a>
|
||||
{/* <div className="text-xs text-gray-400">
|
||||
uploaded {latestUpload}
|
||||
</div> */}
|
||||
{latest.url ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleViewFile(latest.url!)}
|
||||
disabled={isPending}
|
||||
className="underline underline-offset-2 hover:no-underline disabled:opacity-50"
|
||||
>
|
||||
{isPending ? "Loading..." : "View latest file"}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-gray-400">Latest file has no URL</span>
|
||||
)}
|
||||
{latestUpload && (
|
||||
<div className="text-xs text-gray-400">
|
||||
uploaded {latestUpload}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{count} file{count !== 1 && "s"} on record
|
||||
|
|
@ -90,22 +194,26 @@ const latestFile = files.length > 0 ? files.reduce((acc, cur) => {
|
|||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-6 py-4 text-sm text-right w-1/6">
|
||||
<BrandButton
|
||||
label="Upload"
|
||||
onClick={() => setShowUploadModal(true)}
|
||||
backgroundColor="brandblue"
|
||||
/>
|
||||
<UploadModal
|
||||
open={showUploadModal}
|
||||
onClose={() => {
|
||||
setShowUploadModal(false);
|
||||
router.refresh();
|
||||
}}
|
||||
documentType={reportType}
|
||||
uprn={uprn}
|
||||
/>
|
||||
</TableCell>
|
||||
{reportType ? (
|
||||
<TableCell className="px-6 py-4 text-sm text-right w-1/6">
|
||||
<BrandButton
|
||||
label="Upload"
|
||||
onClick={() => setShowUploadModal(true)}
|
||||
backgroundColor="brandblue"
|
||||
/>
|
||||
<UploadModal
|
||||
open={showUploadModal}
|
||||
onClose={() => {
|
||||
setShowUploadModal(false);
|
||||
router.refresh();
|
||||
}}
|
||||
documentType={reportType}
|
||||
uprn={uprn}
|
||||
/>
|
||||
</TableCell>
|
||||
) : (
|
||||
<TableCell className="px-6 py-4 text-sm text-right w-1/6"></TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { DocumentSection } from "./DocumentSection";
|
|||
import {
|
||||
type ReportType,
|
||||
REPORT_TYPES,
|
||||
dbLabelToReportType, // <-- import the map
|
||||
dbLabelToReportType,
|
||||
} from "@/app/db/surveyDB/schema/documents";
|
||||
import type { getUploadedFile } from "@/app/db/surveyDB/schema/surveyDB";
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ export const DocumentsTable: React.FC<Props> = ({
|
|||
const map: Partial<Record<ReportType, getUploadedFile[]>> = {};
|
||||
|
||||
for (const file of uploadedFilesData ?? []) {
|
||||
const uiKey = dbLabelToReportType[file.docType]; // map DB → UI
|
||||
const uiKey = dbLabelToReportType[file.docType]; // map DB → UI. We may not have a docType for surveyor files
|
||||
if (!uiKey) continue; // unknown/legacy type? skip safely
|
||||
|
||||
(map[uiKey] ??= []).push(file);
|
||||
|
|
@ -45,14 +45,11 @@ export const DocumentsTable: React.FC<Props> = ({
|
|||
return map;
|
||||
}, [uploadedFilesData]);
|
||||
|
||||
console.log("filesByType", filesByType);
|
||||
|
||||
return (
|
||||
<Table className="min-w-full table-fixed divide-y divide-gray-200 shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<TableBody className="bg-white divide-y divide-gray-200">
|
||||
{REPORT_TYPES.map((reportType) => {
|
||||
const filesForType = filesByType[reportType] ?? [];
|
||||
console.log("reportType", reportType);
|
||||
return (
|
||||
<React.Fragment key={reportType}>
|
||||
<DocumentSection
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from "@/app/shadcn_components/ui/table";
|
||||
import { DocumentSection } from "./DocumentSection";
|
||||
import { FilesFromSurveyor } from "@/app/db/schema/files_from_surveyor";
|
||||
|
||||
type Props = {
|
||||
uprn: string;
|
||||
files: FilesFromSurveyor[];
|
||||
};
|
||||
|
||||
export const GenericDocumentsTable: React.FC<Props> = ({ uprn, files }) => {
|
||||
return (
|
||||
<Table className="min-w-full table-fixed divide-y divide-gray-200 shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<TableBody className="bg-white divide-y divide-gray-200">
|
||||
{files.map((file, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<DocumentSection
|
||||
reportType={null}
|
||||
uprn={uprn}
|
||||
files={[file]} // array of rows
|
||||
/>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={3} className="h-3 p-0" />
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
import { getPropertyMeta } from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { DocumentsTable } from "./DocumentsTable";
|
||||
import { GenericDocumentsTable } from "./GenericDocumentsTable";
|
||||
import { surveyDB } from "@/app/db/surveyDB/connection";
|
||||
import { db } from "@/app/db/db";
|
||||
import { filesFromSurveyor } from "@/app/db/schema/files_from_surveyor";
|
||||
import type { FilesFromSurveyor } from "@/app/db/schema/files_from_surveyor";
|
||||
import { uploadedFiles } from "@/app/db/surveyDB/schema/surveyDB";
|
||||
import { type getUploadedFiles } from "@/app/db/surveyDB/schema/surveyDB";
|
||||
|
||||
|
|
@ -13,6 +17,23 @@ async function getDocuments(uprn: number): Promise<getUploadedFiles> {
|
|||
return result;
|
||||
}
|
||||
|
||||
async function getSurveyorDocuments(
|
||||
portfolioId: string,
|
||||
propertyId: string
|
||||
): Promise<FilesFromSurveyor[]> {
|
||||
const files = await db
|
||||
.select()
|
||||
.from(filesFromSurveyor)
|
||||
.where(
|
||||
and(
|
||||
eq(filesFromSurveyor.portfolioId, BigInt(portfolioId)),
|
||||
eq(filesFromSurveyor.propertyId, BigInt(propertyId))
|
||||
)
|
||||
);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export default async function DocumentsPage(props: {
|
||||
params: Promise<{ slug: string; propertyId: string }>;
|
||||
}) {
|
||||
|
|
@ -26,7 +47,8 @@ export default async function DocumentsPage(props: {
|
|||
const propertyMeta = await getPropertyMeta(propertyId);
|
||||
const uploadedFiles = await getDocuments(propertyMeta.uprn);
|
||||
|
||||
console.log("Uploaded files:", uploadedFiles);
|
||||
// We also fetch surveyor documents, which is a temp solution
|
||||
const surveyorDocuments = await getSurveyorDocuments(params.slug, propertyId);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -40,6 +62,18 @@ export default async function DocumentsPage(props: {
|
|||
uploadedFilesData={uploadedFiles}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-4"></div>
|
||||
|
||||
<div className="flex items-center justify-between py-4 px-6 bg-brandblue text-white font-semibold text-lg rounded-md">
|
||||
Surveyor Uploaded Documents
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<GenericDocumentsTable
|
||||
uprn={propertyMeta.uprn.toString()}
|
||||
files={surveyorDocuments}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-4"></div>
|
||||
|
||||
<div className="flex items-center justify-between py-4 px-6 bg-brandblue text-white font-semibold text-lg rounded-md">
|
||||
Coordination
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const UploadPage: React.FC = () => {
|
|||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
console.log(data, "hello");
|
||||
setFiles(data.files);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ export default function LoadingPage(props: {
|
|||
Building your retrofit plan
|
||||
</h2>
|
||||
<p className="text-slate-600 mb-3 text-base">
|
||||
Domna IQ is analysing your data and generating your plan summary.
|
||||
Ara is analysing your data and generating your plan summary.
|
||||
</p>
|
||||
|
||||
<div className="relative w-full h-2 bg-gray-200 rounded-full overflow-hidden mb-6">
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export default function RemoteAssessmentClient({
|
|||
{/* Hero text split to use horizontal space better */}
|
||||
<div className="flex flex-col md:flex-row justify-between gap-10">
|
||||
<p className="text-lg md:text-xl text-white/90 leading-relaxed max-w-2xl">
|
||||
Domna IQ analyses your property data, models retrofit options, and
|
||||
Ara analyses your property data, models retrofit options, and
|
||||
estimates potential funding — all without an on-site survey.
|
||||
</p>
|
||||
<p className="text-sm text-white/70 max-w-md md:text-right">
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ module.exports = {
|
|||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
"domna-gradient":
|
||||
"linear-gradient(135deg, #14163d 0%, #2d348f 45%, #3943b7 70%, #eff6fc 100%)",
|
||||
},
|
||||
"domna-gradient":
|
||||
"linear-gradient(135deg, #14163d 0%, #2d348f 45%, #3943b7 70%, #eff6fc 100%)",
|
||||
},
|
||||
colors: {
|
||||
tremor: {
|
||||
brand: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue