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:
Binary file not shown.
@ -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"
|
||||
},
|
||||
|
@ -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")]
|
||||
|
@ -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.
BIN
frontend/src/assets/fonts/Switzer-Variable.ttf
Normal file
BIN
frontend/src/assets/fonts/Switzer-Variable.ttf
Normal file
Binary file not shown.
26
frontend/src/components/search-card/SearchCardContact.tsx
Normal file
26
frontend/src/components/search-card/SearchCardContact.tsx
Normal 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>
|
||||
);
|
||||
};
|
33
frontend/src/components/search-card/SearchCardEvent.tsx
Normal file
33
frontend/src/components/search-card/SearchCardEvent.tsx
Normal 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>
|
||||
);
|
||||
};
|
26
frontend/src/components/search-card/SearchCardLocation.tsx
Normal file
26
frontend/src/components/search-card/SearchCardLocation.tsx
Normal 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>
|
||||
);
|
||||
};
|
26
frontend/src/components/search-card/SearchCardNote.tsx
Normal file
26
frontend/src/components/search-card/SearchCardNote.tsx
Normal 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>
|
||||
);
|
||||
};
|
30
frontend/src/components/search-card/SearchCardReceipt.tsx
Normal file
30
frontend/src/components/search-card/SearchCardReceipt.tsx
Normal 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>
|
||||
);
|
||||
};
|
26
frontend/src/components/search-card/SearchCardWebsite.tsx
Normal file
26
frontend/src/components/search-card/SearchCardWebsite.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
];
|
436
frontend/src/network/sampleData.ts
Normal file
436
frontend/src/network/sampleData.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
];
|
@ -21,7 +21,10 @@ interface ReceiptItem {
|
||||
}
|
||||
|
||||
interface DataItemType<T extends string, D> {
|
||||
id: string;
|
||||
title: string;
|
||||
type: T;
|
||||
rawData: string;
|
||||
data: D;
|
||||
}
|
||||
|
||||
|
20
frontend/src/utils/getCardSize.ts
Normal file
20
frontend/src/utils/getCardSize.ts
Normal 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";
|
||||
}
|
||||
};
|
@ -4,7 +4,7 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["Manrope", "sans-serif"],
|
||||
sans: ["Switzer", "sans-serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Reference in New Issue
Block a user