feat(frontend): add new search card components and update styling

- Introduced new search card components for Contact, Event, Location, Note, Receipt, and Website.
- Updated the App component to utilize these new components for displaying search results.
- Changed the default font from Manrope to Switzer and updated related styles.
- Added a new dependency `solid-motionone` to package.json.
- Improved search functionality with a new sample data structure and enhanced search logic.
This commit is contained in:
2025-03-23 21:56:09 +01:00
parent 4c85f1de79
commit caf168c7a1
18 changed files with 791 additions and 205 deletions

Binary file not shown.

View File

@ -24,6 +24,7 @@
"clsx": "^2.1.1",
"fuse.js": "^7.1.0",
"solid-js": "^1.9.3",
"solid-motionone": "^1.0.3",
"tailwind-scrollbar-hide": "^2.0.0",
"valibot": "^1.0.0-rc.2"
},

View File

@ -116,6 +116,7 @@ pub fn run() {
.setup(|app| {
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.inner_size(480.0, 360.0)
.hidden_title(true)
.resizable(true);
// set transparent title bar only when building for macOS
#[cfg(target_os = "macos")]

View File

@ -1,129 +1,123 @@
import { Search } from "@kobalte/core/search";
import { useNavigate } from "@solidjs/router";
import { IconRefresh, IconSearch } from "@tabler/icons-solidjs";
import { IconSearch } from "@tabler/icons-solidjs";
import clsx from "clsx";
import Fuse from "fuse.js";
import { createEffect, createResource, createSignal } from "solid-js";
import { getUserImages } from "./network";
import { For, createSignal } from "solid-js";
import { SearchCardContact } from "./components/search-card/SearchCardContact";
import { SearchCardEvent } from "./components/search-card/SearchCardEvent";
import { SearchCardLocation } from "./components/search-card/SearchCardLocation";
import { SearchCardNote } from "./components/search-card/SearchCardNote";
import { SearchCardReceipt } from "./components/search-card/SearchCardReceipt";
import { SearchCardWebsite } from "./components/search-card/SearchCardWebsite";
import { sampleData } from "./network/sampleData";
import type { DataItem } from "./network/types";
import { getCardSize } from "./utils/getCardSize";
type UserImages = Awaited<ReturnType<typeof getUserImages>>;
const getCardComponent = (item: DataItem) => {
switch (item.type) {
case "Location":
return <SearchCardLocation item={item} />;
case "Event":
return <SearchCardEvent item={item} />;
case "Contact":
return <SearchCardContact item={item} />;
case "Website":
return <SearchCardWebsite item={item} />;
case "Note":
return <SearchCardNote item={item} />;
case "Receipt":
return <SearchCardReceipt item={item} />;
default:
return null;
}
};
function App() {
const [searchResults, setSearchResults] = createSignal<
UserImages[number]["Text"]
>([]);
const [searchResults, setSearchResults] = createSignal<DataItem[]>([]);
const [searchQuery, setSearchQuery] = createSignal("");
const [selectedItem, setSelectedItem] = createSignal<DataItem | null>(null);
const [images] = createResource(getUserImages);
const nav = useNavigate();
let fuze = new Fuse<NonNullable<UserImages[number]["Text"]>[number]>([], {
keys: ["ImageText"],
const fuze = new Fuse<DataItem>(sampleData, {
keys: [{ name: "title", weight: 2 }, "rawData"],
threshold: 0.4,
});
// TODO: there's probably a better way?
createEffect(() => {
const userImages = images();
if (userImages == null) {
return;
}
const imageText = userImages.flatMap((i) => i.Text ?? []);
fuze = new Fuse(imageText, {
keys: ["ImageText"],
threshold: 0.3,
});
});
const onInputChange = (query: string) => {
// TODO: we can migrate this searching to Rust, so we don't abuse the main thread.
// But, it's not too bad as is.
setSearchResults(fuze.search(query).flatMap((s) => s.item));
const onInputChange = (event: InputEvent) => {
const query = (event.target as HTMLInputElement).value;
setSearchQuery(query);
setSearchResults(fuze.search(query).map((s) => s.item));
};
return (
<main class="container pt-2">
<Search
triggerMode="focus"
options={searchResults() ?? []}
onInputChange={onInputChange}
onChange={(item) => {
if (item?.ImageID == null) {
console.error("ImageID was null");
return;
}
nav(`/image/${item.ImageID}`);
}}
optionValue="ID"
optionLabel="ImageText"
placeholder="Search for stuff..."
itemComponent={(props) => (
<Search.Item
item={props.item}
class="col-span-3 row-span-3 bg-red-200 rounded-xl"
>
<Search.ItemLabel class="mx-[-100px]">
{props.item.rawValue.ImageText ?? ""}
</Search.ItemLabel>
</Search.Item>
)}
>
<>
<main class="container pt-2">
<div class="px-4">
<Search.Control class="inline-flex justify-between w-full rounded-xl text-base leading-none outline-none bg-white border border-gray-200 text-gray-900 transition-colors duration-250 ui-invalid:border-red-500 ui-invalid:text-red-500">
<Search.Indicator
class="appearance-none inline-flex justify-center items-center w-auto outline-none rounded-l-md px-2.5 text-gray-900 text-base leading-none transition-colors duration-250"
loadingComponent={
<Search.Icon class="h-5 w-5 grid justify-items-center flex-none">
<IconRefresh
size={20}
class="m-auto animate-spin"
/>
</Search.Icon>
}
>
<Search.Icon class="h-5 w-5 grid justify-items-center flex-none">
<IconSearch class="m-auto size-5 text-gray-600" />
</Search.Icon>
</Search.Indicator>
<Search.Input class="appearance-none inline-flex w-full min-h-[40px] text-base bg-transparent rounded-l-md outline-none placeholder:text-gray-600" />
</Search.Control>
</div>
<div class="h-[254px] mt-4 overflow-scroll scrollbar-hide">
<Search.Listbox class="w-full grid grid-cols-9 grid-rows-9 gap-2 h-[480px] grid-flow-row-dense py-4" />
{/* <Search.NoResult class="text-center p-2 pb-6 m-auto text-gray-600">
No results found
</Search.NoResult> */}
</div>
</Search>
{/* <div class="px-4 mt-4 bg-white rounded-t-2xl">
<div class="col-span-3 row-span-3 bg-red-200 rounded-xl" />
<div class="col-span-3 row-span-3 bg-green-200 rounded-xl" />
<div class="col-span-6 row-span-3 bg-yellow-200 rounded-xl" />
<div class="col-span-3 row-span-3 bg-green-200 rounded-xl" />
<div class="col-span-3 row-span-3 bg-blue-200 rounded-xl" />
<div class="col-span-3 row-span-3 bg-green-200 rounded-xl" />
<div class="col-span-6 row-span-3 bg-yellow-200 rounded-xl" />
<For each={images()}>
{(image) => (
<A href={`/image/${image.ID}`}>
<img
src={`http://localhost:3040/image/${image.ID}`}
class="col-span-3 row-span-3 rounded-xl"
alt=""
<div class="inline-flex justify-between w-full rounded-xl text-base leading-none outline-none bg-white border border-neutral-200 text-neutral-900">
<div class="appearance-none inline-flex justify-center items-center w-auto outline-none rounded-l-md px-2.5 text-gray-900">
<IconSearch
size={20}
class="m-auto size-5 text-neutral-600"
/>
</A>
)}
</For>
</div> */}
<div class="w-full border-t h-10 bg-white px-4 flex items-center border-neutral-100">
footer
</div>
</main>
</div>
<input
type="text"
value={searchQuery()}
onInput={onInputChange}
placeholder="Search for stuff..."
class="appearance-none inline-flex w-full min-h-[40px] text-base bg-transparent rounded-l-md outline-none placeholder:text-gray-600"
/>
</div>
</div>
<div class="px-4 mt-4 bg-white rounded-t-2xl">
<div class="h-[254px] mt-4 overflow-scroll scrollbar-hide">
{searchResults().length > 0 ? (
<div class="w-full grid grid-cols-9 gap-2 grid-flow-row-dense py-4">
<For each={searchResults()}>
{(item) => (
<div
onClick={() =>
setSelectedItem(item)
}
onKeyDown={(e) => {
if (e.key === "Enter") {
setSelectedItem(item);
}
}}
class={clsx(
"h-[144px] border relative border-neutral-200 cursor-pointer overflow-hidden rounded-xl",
{
"col-span-3":
getCardSize(
item.type,
) === "1/1",
"col-span-6":
getCardSize(
item.type,
) === "2/1",
},
)}
>
<span class="sr-only">
{item.title}
</span>
{getCardComponent(item)}
</div>
)}
</For>
</div>
) : searchQuery() !== "" ? (
<div class="text-center text-lg m-auto text-neutral-700">
No results found
</div>
) : null}
</div>
</div>
<div class="w-full border-t h-10 bg-white px-4 flex items-center border-neutral-100">
footer
</div>
</main>
</>
);
}

Binary file not shown.

View File

@ -0,0 +1,26 @@
import { Separator } from "@kobalte/core/separator";
import { IconUser } from "@tabler/icons-solidjs";
import type { Contact } from "../../network/types";
type Props = {
item: Contact;
};
export const SearchCardContact = ({ item }: Props) => {
const { data } = item;
return (
<div class="absolute inset-0 p-3 bg-orange-50">
<div class="grid grid-cols-[auto_20px] gap-1 mb-1">
<p class="text-sm text-neutral-900 font-bold">{data.name}</p>
<IconUser size={20} class="text-neutral-500 mt-1" />
</div>
<p class="text-xs text-neutral-500">{data.phoneNumber}</p>
<Separator class="my-2" />
<p class="text-xs text-neutral-500 line-clamp-2 overflow-hidden">
{data.notes}
</p>
</div>
);
};

View File

@ -0,0 +1,33 @@
import { Separator } from "@kobalte/core/separator";
import { IconCalendar } from "@tabler/icons-solidjs";
import type { Event } from "../../network/types";
type Props = {
item: Event;
};
export const SearchCardEvent = ({ item }: Props) => {
const { data } = item;
return (
<div class="absolute inset-0 p-3 bg-purple-50">
<div class="grid grid-cols-[auto_20px] gap-1 mb-1">
<p class="text-sm text-neutral-900 font-bold">{data.title}</p>
<IconCalendar size={20} class="text-neutral-500 mt-1" />
</div>
<p class="text-xs text-neutral-500">
Organized by {data.organizer.name} on{" "}
{new Date(data.dateTime.start).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})}
</p>
<Separator class="my-2" />
<p class="text-xs text-neutral-500 line-clamp-2 overflow-hidden">
{data.description}
</p>
</div>
);
};

View File

@ -0,0 +1,26 @@
import { Separator } from "@kobalte/core/separator";
import { IconMapPin } from "@tabler/icons-solidjs";
import type { Location } from "../../network/types";
type Props = {
item: Location;
};
export const SearchCardLocation = ({ item }: Props) => {
const { data } = item;
return (
<div class="absolute inset-0 p-3 bg-red-50">
<div class="grid grid-cols-[auto_20px] gap-1 mb-1">
<p class="text-sm text-neutral-900 font-bold">{data.name}</p>
<IconMapPin size={20} class="text-neutral-500 mt-1" />
</div>
<p class="text-xs text-neutral-500">{data.address}</p>
<Separator class="my-2" />
<p class="text-xs text-neutral-500 line-clamp-2 overflow-hidden">
{data.description}
</p>
</div>
);
};

View File

@ -0,0 +1,26 @@
import { Separator } from "@kobalte/core/separator";
import { IconNote } from "@tabler/icons-solidjs";
import type { Note } from "../../network/types";
type Props = {
item: Note;
};
export const SearchCardNote = ({ item }: Props) => {
const { data } = item;
return (
<div class="absolute inset-0 p-3 bg-green-50">
<div class="grid grid-cols-[auto_20px] gap-1 mb-1">
<p class="text-sm text-neutral-900 font-bold">{data.title}</p>
<IconNote size={20} class="text-neutral-500 mt-1" />
</div>
<p class="text-xs text-neutral-500">{data.keywords}</p>
<Separator class="my-2" />
<p class="text-xs text-neutral-500 line-clamp-2 overflow-hidden">
{data.content}
</p>
</div>
);
};

View File

@ -0,0 +1,30 @@
import { Separator } from "@kobalte/core/separator";
import { IconReceipt } from "@tabler/icons-solidjs";
import type { Receipt } from "../../network/types";
type Props = {
item: Receipt;
};
export const SearchCardReceipt = ({ item }: Props) => {
const { data } = item;
return (
<div class="absolute inset-0 p-3 bg-yellow-50">
<div class="grid grid-cols-[auto_20px] gap-1 mb-1">
<p class="text-sm text-neutral-900 font-bold">
{data.orderNumber} - {data.vendor}
</p>
<IconReceipt size={20} class="text-neutral-500 mt-1" />
</div>
<p class="text-xs text-neutral-500">
{data.shippingAddress.address}
</p>
<Separator class="my-2" />
<p class="text-xs text-neutral-500 line-clamp-2 overflow-hidden">
{data.amount} {data.currency}
</p>
</div>
);
};

View File

@ -0,0 +1,26 @@
import { Separator } from "@kobalte/core/separator";
import { IconLink } from "@tabler/icons-solidjs";
import type { Website } from "../../network/types";
type Props = {
item: Website;
};
export const SearchCardWebsite = ({ item }: Props) => {
const { data } = item;
return (
<div class="absolute inset-0 p-3 bg-blue-50">
<div class="grid grid-cols-[auto_20px] gap-1 mb-1">
<p class="text-sm text-neutral-900 font-bold">{data.title}</p>
<IconLink size={20} class="text-neutral-500 mt-1" />
</div>
<p class="text-xs text-neutral-500">{data.url}</p>
<Separator class="my-2" />
<p class="text-xs text-neutral-500 line-clamp-2 overflow-hidden">
{data.description}
</p>
</div>
);
};

View File

@ -2,16 +2,68 @@
@tailwind components;
@tailwind utilities;
/* CSS Reset */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
line-height: 1.2;
-webkit-font-smoothing: antialiased;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
#root,
#__next {
isolation: isolate;
}
@font-face {
font-family: "Manrope";
src: url("./assets/fonts/Manrope-VariableFont_wght.ttf") format("truetype");
font-family: "Switzer";
src: url("./assets/fonts/Switzer-Variable.ttf") format("truetype");
font-weight: 100 900;
font-display: swap;
}
:root {
@apply bg-neutral-100 text-black rounded-xl;
font-family: Manrope, sans-serif;
font-family: "Switzer", sans-serif;
font-stretch: 100%;
font-optical-sizing: auto;
font-size: 16px;
line-height: 24px;
font-weight: 500;

View File

@ -1,88 +0,0 @@
import type { DataArray } from "./types";
export const sampleData: DataArray = [
{
type: "Event",
data: {
title: "Startup Mixer",
dateTime: {
start: "2025-06-01T19:00:00+01:00",
end: "2025-06-01T23:00:00+01:00",
},
location: "The Kings Arms, 27 Ropemaker St, London EC2Y 9LY",
description:
"Casual networking event for tech entrepreneurs and investors in London.",
organizer: {
name: "London Startup Network",
email: "events@londonstartupnetwork.co.uk",
},
attendees: ["Alex Smith", "Sofia Rodriguez"],
category: "Networking",
},
},
{
type: "Contact",
data: {
name: "João Silva",
phoneNumber: "+351 912 345 678",
emailAddress: "joao.silva@example.pt",
address: "Rua do Carmo 12, 1200-161 Lisboa, Portugal",
organization: "PortoTech Solutions",
title: "Marketing Manager",
notes: "Met at Web Summit Lisbon 2024",
},
},
{
type: "Location",
data: {
name: "Barman Dictat",
address: "14 Khreshchatyk St., Kyiv, Ukraine",
category: "Bar",
description:
"Stylish cocktail bar in the heart of Kyiv with an extensive menu of craft cocktails and a sophisticated atmosphere",
},
},
{
type: "Note",
data: {
title: "Q2 2025 Marketing Strategy",
keywords: ["strategy", "digital marketing", "Q2", "2025"],
content:
"## Executive Summary\n\nOur Q2 2025 marketing strategy focuses on expanding our digital presence and increasing customer engagement across all platforms. We will leverage AI-driven personalization to enhance user experience and implement a multi-channel approach to reach our target demographics more effectively.\n\n### Key Objectives\n\n1. Increase website traffic by 30% through SEO optimization and content marketing.\n2. Boost social media engagement rates by 25% using interactive campaigns and influencer partnerships.\n3. Implement a new customer loyalty program to improve retention rates by 15%.\n\nBy aligning our marketing efforts with emerging trends and customer preferences, we aim to solidify our market position and drive sustainable growth throughout Q2 and beyond.",
},
},
{
type: "Website",
data: {
url: "https://www.attio.com",
title: "Attio",
description:
"Attio is the AI-native CRM that builds, scales and grows your company to the next level.",
category: "SaaS",
},
},
{
type: "Receipt",
data: {
receiptDate: "2025-03-12T20:15:30+01:00",
orderNumber: "ORD12345",
amount: 49.99,
currency: "GBP",
vendor: "Zara Online Store",
items: [
{
name: "Slim Fit Dress Shirt",
quantity: 1,
price: 49.99,
currency: "GBP",
},
],
paymentMethod: "Visa",
shippingAddress: {
name: "Alex Smith",
address: "123 High St, London, EC2A 3AZ",
},
category: "Online Shopping",
},
},
];

View File

@ -0,0 +1,436 @@
import type { DataArray, DataItem } from "./types";
const getRawData = (item: DataItem) => {
return Object.values(item.data).join(" ");
};
export const sampleData: DataArray = [
{
id: "1",
type: "Event",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.title;
},
data: {
title: "Startup Mixer",
dateTime: {
start: "2025-06-01T19:00:00+01:00",
end: "2025-06-01T23:00:00+01:00",
},
location: "The Kings Arms, 27 Ropemaker St, London EC2Y 9LY",
description:
"Casual networking event for tech entrepreneurs and investors in London.",
organizer: {
name: "London Startup Network",
email: "events@londonstartupnetwork.co.uk",
},
attendees: ["Alex Smith", "Sofia Rodriguez"],
category: "Networking",
},
},
{
id: "2",
type: "Contact",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.name;
},
data: {
name: "João Silva",
phoneNumber: "+351 912 345 678",
emailAddress: "joao.silva@example.pt",
address: "Rua do Carmo 12, 1200-161 Lisboa, Portugal",
organization: "PortoTech Solutions",
title: "Marketing Manager",
notes: "Met at Web Summit Lisbon 2024",
},
},
{
id: "3",
type: "Location",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.name;
},
data: {
name: "Barman Dictat",
address: "14 Khreshchatyk St., Kyiv, Ukraine",
category: "Bar",
description:
"Stylish cocktail bar in the heart of Kyiv with an extensive menu of craft cocktails and a sophisticated atmosphere",
},
},
{
id: "4",
type: "Note",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.title;
},
data: {
title: "Q2 2025 Marketing Strategy",
keywords: ["strategy", "digital marketing", "Q2", "2025"],
content:
"## Executive Summary\n\nOur Q2 2025 marketing strategy focuses on expanding our digital presence and increasing customer engagement across all platforms. We will leverage AI-driven personalization to enhance user experience and implement a multi-channel approach to reach our target demographics more effectively.\n\n### Key Objectives\n\n1. Increase website traffic by 30% through SEO optimization and content marketing.\n2. Boost social media engagement rates by 25% using interactive campaigns and influencer partnerships.\n3. Implement a new customer loyalty program to improve retention rates by 15%.\n\nBy aligning our marketing efforts with emerging trends and customer preferences, we aim to solidify our market position and drive sustainable growth throughout Q2 and beyond.",
},
},
{
id: "5",
type: "Website",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.title;
},
data: {
url: "https://www.attio.com",
title: "Attio",
description:
"Attio is the AI-native CRM that builds, scales and grows your company to the next level.",
category: "SaaS",
},
},
{
id: "6",
type: "Receipt",
get rawData() {
return getRawData(this);
},
get title() {
return `${this.data.orderNumber} - ${this.data.vendor}`;
},
data: {
receiptDate: "2025-03-12T20:15:30+01:00",
orderNumber: "ORD12345",
amount: 49.99,
currency: "GBP",
vendor: "Zara Online Store",
items: [
{
name: "Slim Fit Dress Shirt",
quantity: 1,
price: 49.99,
currency: "GBP",
},
],
paymentMethod: "Visa",
shippingAddress: {
name: "Alex Smith",
address: "123 High St, London, EC2A 3AZ",
},
category: "Online Shopping",
},
},
{
id: "7",
type: "Event",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.title;
},
data: {
title: "AI in Healthcare Summit",
dateTime: {
start: "2025-07-15T09:00:00+01:00",
end: "2025-07-15T17:00:00+01:00",
},
location:
"Royal College of Physicians, 11 St Andrews Pl, London NW1 4LE",
description:
"Annual conference exploring the latest developments in AI applications for healthcare",
organizer: {
name: "HealthTech Alliance",
email: "events@healthtechalliance.org",
},
attendees: [
"Dr. Sarah Chen",
"Prof. James Wilson",
"Dr. Maria Santos",
],
category: "Conference",
},
},
{
id: "8",
type: "Contact",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.name;
},
data: {
name: "Emma Schmidt",
phoneNumber: "+49 30 12345678",
emailAddress: "e.schmidt@techberlin.de",
address: "Friedrichstraße 123, 10117 Berlin, Germany",
organization: "TechBerlin GmbH",
title: "Chief Technology Officer",
notes: "Key contact for Berlin tech scene, met at TechFest 2024",
},
},
{
id: "9",
type: "Location",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.name;
},
data: {
name: "Digital Nomad Hub",
address: "Calle Princesa 25, 08001 Barcelona, Spain",
category: "Coworking Space",
description:
"Modern coworking space with high-speed internet, meeting rooms, and a vibrant community of international remote workers",
},
},
{
id: "10",
type: "Website",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.title;
},
data: {
url: "https://www.techcrunch.com",
title: "TechCrunch",
description:
"Leading technology media platform covering startups, tech news, and venture capital",
category: "Tech News",
},
},
{
id: "11",
type: "Note",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.title;
},
data: {
title: "Product Roadmap 2025",
keywords: ["product development", "strategy", "features", "2025"],
content:
"## Overview\n\nPriority features for 2025:\n\n1. AI-powered customer insights dashboard\n2. Integration with major CRM platforms\n3. Mobile app redesign\n4. Enhanced analytics suite\n\n### Timeline\n- Q1: Research and planning\n- Q2: Development phase 1\n- Q3: Beta testing\n- Q4: Full release",
},
},
{
id: "12",
type: "Receipt",
get rawData() {
return getRawData(this);
},
get title() {
return `${this.data.orderNumber} - ${this.data.vendor}`;
},
data: {
receiptDate: "2025-03-15T13:45:00+01:00",
orderNumber: "INV789012",
amount: 1299.99,
currency: "EUR",
vendor: "Apple Store",
items: [
{
name: "MacBook Air M3",
quantity: 1,
price: 1299.99,
currency: "EUR",
},
],
paymentMethod: "MasterCard",
shippingAddress: {
name: "Emma Schmidt",
address: "Friedrichstraße 123, 10117 Berlin, Germany",
},
category: "Electronics",
},
},
{
id: "13",
type: "Event",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.title;
},
data: {
title: "Sustainable Tech Workshop",
dateTime: {
start: "2025-08-20T14:00:00+02:00",
end: "2025-08-20T17:00:00+02:00",
},
location: "GreenTech Hub, Prinsengracht 150, Amsterdam",
description:
"Workshop on implementing sustainable practices in tech companies",
organizer: {
name: "Green Digital Alliance",
email: "workshops@greendigital.org",
},
attendees: ["Lisa van der Berg", "Mark Johnson"],
category: "Workshop",
},
},
{
id: "14",
type: "Contact",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.name;
},
data: {
name: "Akiko Tanaka",
phoneNumber: "+81 3 1234 5678",
emailAddress: "a.tanaka@tokyotech.jp",
address: "2-1-1 Marunouchi, Chiyoda-ku, Tokyo 100-0005",
organization: "Tokyo Tech Ventures",
title: "Investment Director",
notes: "Specialist in Asia-Pacific tech investments",
},
},
{
id: "15",
type: "Location",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.name;
},
data: {
name: "Innovation Center Stockholm",
address: "Regeringsgatan 65, 111 56 Stockholm, Sweden",
category: "Tech Hub",
description:
"Leading innovation center in Scandinavia, hosting startups and tech events",
},
},
{
id: "16",
type: "Website",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.title;
},
data: {
url: "https://www.github.com",
title: "GitHub",
description: "World's leading software development platform",
category: "Development Tools",
},
},
{
id: "17",
type: "Note",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.title;
},
data: {
title: "Team Structure Reorganization",
keywords: ["organization", "teams", "structure", "management"],
content:
"## Proposed Changes\n\n1. Create dedicated AI/ML team\n2. Merge frontend and mobile teams\n3. Establish DevOps center of excellence\n\n### Timeline\nGradual implementation over Q3 2025\n\n### Expected Outcomes\n- Improved efficiency\n- Better resource allocation\n- Faster delivery cycles",
},
},
{
id: "18",
type: "Receipt",
get rawData() {
return getRawData(this);
},
get title() {
return `${this.data.orderNumber} - ${this.data.vendor}`;
},
data: {
receiptDate: "2025-03-18T10:30:00+01:00",
orderNumber: "BOK456789",
amount: 850.0,
currency: "USD",
vendor: "Hilton Hotels",
items: [
{
name: "Executive Suite - 2 nights",
quantity: 1,
price: 850.0,
currency: "USD",
},
],
paymentMethod: "American Express",
shippingAddress: {
name: "Akiko Tanaka",
address:
"Hilton San Francisco, 333 O'Farrell St, San Francisco, CA 94102",
},
category: "Travel",
},
},
{
id: "19",
type: "Event",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.title;
},
data: {
title: "Web3 Developer Conference",
dateTime: {
start: "2025-09-10T09:00:00-07:00",
end: "2025-09-12T17:00:00-07:00",
},
location: "Moscone Center, 747 Howard St, San Francisco, CA 94103",
description:
"Three-day conference focusing on blockchain, DeFi, and Web3 development",
organizer: {
name: "Web3 Alliance",
email: "conference@web3alliance.org",
},
attendees: ["Vitalik B.", "Charles H.", "Gavin W."],
category: "Conference",
},
},
{
id: "20",
type: "Contact",
get rawData() {
return getRawData(this);
},
get title() {
return this.data.name;
},
data: {
name: "Rachel Chen",
phoneNumber: "+1 415 555 0123",
emailAddress: "rachel.chen@siliconvc.com",
address: "525 Market St, San Francisco, CA 94105",
organization: "Silicon Valley Capital",
title: "Partner",
notes: "Specializes in Series A/B investments in AI and ML startups",
},
},
];

View File

@ -21,7 +21,10 @@ interface ReceiptItem {
}
interface DataItemType<T extends string, D> {
id: string;
title: string;
type: T;
rawData: string;
data: D;
}

View File

@ -0,0 +1,20 @@
export type CardSize = "1/1" | "2/1";
export const getCardSize = (type: string): CardSize => {
switch (type) {
case "Event":
return "2/1";
case "Contact":
return "1/1";
case "Location":
return "1/1";
case "Note":
return "2/1";
case "Website":
return "1/1";
case "Receipt":
return "2/1";
default:
return "1/1";
}
};

View File

@ -4,7 +4,7 @@ export default {
theme: {
extend: {
fontFamily: {
sans: ["Manrope", "sans-serif"],
sans: ["Switzer", "sans-serif"],
},
},
},