Skip to content

Commit

Permalink
Add bluetooth, dropzone & history hooks (#77)
Browse files Browse the repository at this point in the history
* ✨ Add new hooks for Bluetooth, Memory, Draggable, DropZone, History, and Session Storage; update package.json with new type definitions

* ✨ Enable React compiler in Next.js config; add new hooks for session storage, memory, history, Bluetooth, and drop zone management; update changelog and package list

* ✨ Add demo component for useDraggable hook with placeholder content

* ✨ Add @types/web-bluetooth to package.json and update related files
  • Loading branch information
damien-schneider authored Feb 7, 2025
1 parent cc5be62 commit 3bfbaec
Show file tree
Hide file tree
Showing 34 changed files with 1,061 additions and 38 deletions.
2 changes: 1 addition & 1 deletion apps/website/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ await jiti.import("./src/env.ts");
const nextConfig = {
experimental: {
// serverComponentsExternalPackages: ["shiki", "vscode-oniguruma"],
// reactCompiler: true,
reactCompiler: true,
},
transpilePackages: ["shiki", "next-mdx-remote"],
images: {
Expand Down
1 change: 1 addition & 0 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@types/node": "20.12.7",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.2",
"@types/web-bluetooth": "^0.0.20",
"@vitejs/plugin-react": "4.3.1",
"autoprefixer": "10.4.19",
"eslint": "9.0.0",
Expand Down
6 changes: 6 additions & 0 deletions apps/website/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export default function RootLayout({
}) {
return (
<html className={font.className} lang="en" suppressHydrationWarning={true}>
<head>
{/* <script
crossOrigin="anonymous"
src="//unpkg.com/react-scan/dist/auto.global.js"
/> */}
</head>
<PlausibleScripts />
<Providers>
{/* <body className="dark:bg-[url('/grid-dark-mode.svg')] bg-[url('/grid-light-mode.svg')] dark:bg-gray-950 bg-gray-50"> */}
Expand Down
9 changes: 9 additions & 0 deletions apps/website/src/changelogs/2025-02-08.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: New hooks
---

- A new `useSessionStorage` hook has been added to manage session storage values.
- A new `useMemory` hook has been added to retrieve memory values.
- A new `useHistory` hook has been added to manage history values for advanced applications.
- A new `useBluetooth` hook has been added to manage Bluetooth connections.
- A new `useDropZone` hook has been added to manage drag-and-drop file uploads.
2 changes: 1 addition & 1 deletion apps/website/src/changelogs/last-changelog-date.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// This file is generated by the generate-latest-changelog-date script
export const lastChangelogDate = new Date("2025-01-19T23:00:00.000Z");
export const lastChangelogDate = new Date("2025-02-07T23:00:00.000Z");
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ export const packageCheckListToInstall: PackageToInstallType[] = [
find: [`from "@types/react-dom"`],
packageName: "@types/react-dom",
},
{
find: [`from "@types/web-bluetooth"`],
packageName: "@types/web-bluetooth",
},
{
find: [`from "@vitejs/plugin-react"`],
packageName: "@vitejs/plugin-react",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
BatteryWarningIcon,
InfinityIcon,
} from "lucide-react";
import type React from "react";

import { cn } from "@/cuicui/utils/cn";

type BatteryInfoProps = {
Expand All @@ -18,13 +18,13 @@ type BatteryInfoProps = {
className?: string;
};

export const BatteryIndicator: React.FC<BatteryInfoProps> = ({
export const BatteryIndicator = ({
level,
isCharging,
chargingTime,
dischargingTime,
className,
}) => {
}: BatteryInfoProps) => {
const getBatteryIcon = (level: number | null, isCharging: boolean | null) => {
if (level === null) {
return <BatteryIcon className="size-5 text-orange-500" />;
Expand Down
11 changes: 11 additions & 0 deletions packages/ui/cuicui/hooks/use-bluetooth/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { CategoryMetaType } from "@/lib/types/component";
import { BluetoothIcon } from "lucide-react";

export const category: CategoryMetaType = {
name: "Use Bluetooth",
description: "A hook to use bluetooth",
icon: BluetoothIcon,
latestUpdateDate: new Date("2025-02-08"),
};

export default category;
8 changes: 8 additions & 0 deletions packages/ui/cuicui/hooks/use-bluetooth/hook/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ComponentMetaType } from "@/lib/types/component";

export const component: ComponentMetaType = {
name: "Use Bluetooth",
description: "A hook to use bluetooth",
};

export default component;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { useBluetooth } from "@/cuicui/hooks/use-bluetooth";

function BluetoothComponent() {
const { isSupported, isConnected, device, requestDevice, server, error } =
useBluetooth({
acceptAllDevices: true,
optionalServices: ["battery_service"],
});

if (!isSupported) {
return <div>Bluetooth is not supported</div>;
}

return (
<div className="cuicui-default-style">
<button type="button" onClick={requestDevice}>
Connect to Bluetooth Device
</button>
{isConnected && <div>Connected to: {device?.name}</div>}
{(error as JSON) && (
<>
<p>Error:</p>
<pre>{JSON.stringify(error, null, 2)}</pre>
</>
)}
</div>
);
}

export default BluetoothComponent;
122 changes: 122 additions & 0 deletions packages/ui/cuicui/hooks/use-bluetooth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use client";
//This requires @types/web-bluetooth package to be installed
import { useState, useEffect, useCallback } from "react";

interface UseBluetoothRequestDeviceOptions {
filters?: BluetoothLEScanFilter[];
optionalServices?: BluetoothServiceUUID[];
}

interface UseBluetoothOptions extends UseBluetoothRequestDeviceOptions {
acceptAllDevices?: boolean;
navigator?: Navigator;
}

export function useBluetooth(options?: UseBluetoothOptions) {
const windowVar = typeof window !== "undefined" ? window : undefined;
const {
acceptAllDevices = false,
filters = undefined,
optionalServices = undefined,
navigator = windowVar?.navigator,
} = options || {};

const [device, setDevice] = useState<BluetoothDevice | undefined>();
const [server, setServer] = useState<BluetoothRemoteGATTServer | undefined>();
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState<unknown | null>(null);

const isSupported = Boolean(navigator && "bluetooth" in navigator);

const reset = useCallback(() => {
setIsConnected(false);
setDevice(undefined);
setServer(undefined);
}, []);

const connectToBluetoothGATTServer = useCallback(async () => {
setError(null);

if (device?.gatt) {
try {
const newServer = await device.gatt.connect();
setServer(newServer);
setIsConnected(newServer.connected);
} catch (err) {
setError(err);
}
}
}, [device]);

const requestDevice = async () => {
if (!isSupported) {
return;
}

setError(null);
let effectiveAcceptAllDevices = acceptAllDevices;

if (filters && filters.length > 0) {
effectiveAcceptAllDevices = false;
}

try {
const newDevice = await navigator?.bluetooth.requestDevice({
acceptAllDevices: effectiveAcceptAllDevices,
filters,
optionalServices,
});
setDevice(newDevice);
} catch (err) {
setError(err);
}
};

useEffect(() => {
if (device) {
connectToBluetoothGATTServer();

// Add disconnect listener
const handleDisconnect = () => reset();
device.addEventListener("gattserverdisconnected", handleDisconnect);

return () => {
device.removeEventListener("gattserverdisconnected", handleDisconnect);
};
}
}, [device, connectToBluetoothGATTServer, reset]);

// Connect on mount if device exists
useEffect(() => {
if (device) {
device.gatt?.connect();
}
}, [device]);

// Disconnect on unmount
useEffect(() => {
return () => {
if (device) {
device.gatt?.disconnect();
}
};
}, [device]);

return {
isSupported,
isConnected,
device,
requestDevice,
server,
error,
};
}

export interface UseBluetoothReturn {
isSupported: boolean;
isConnected: boolean;
device: BluetoothDevice | undefined;
requestDevice: () => Promise<void>;
server: BluetoothRemoteGATTServer | undefined;
error: unknown | null;
}
12 changes: 12 additions & 0 deletions packages/ui/cuicui/hooks/use-draggable/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { CategoryMetaType } from "@/lib/types/component";
import { HandIcon } from "lucide-react";

export const category: CategoryMetaType = {
name: "Use Draggable",
description: "A hook to use draggable",
icon: HandIcon,
latestUpdateDate: new Date("2025-02-08"),
isComingSoon: true,
};

export default category;
8 changes: 8 additions & 0 deletions packages/ui/cuicui/hooks/use-draggable/hook/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ComponentMetaType } from "@/lib/types/component";

export const component: ComponentMetaType = {
name: "Use Draggable",
description: "A hook to use draggable",
};

export default component;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Coming soon

export default function useDraggableDemo() {
return(
<div>
Coming soon
</div>
);
}
1 change: 1 addition & 0 deletions packages/ui/cuicui/hooks/use-draggable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//Coming soon
11 changes: 11 additions & 0 deletions packages/ui/cuicui/hooks/use-drop-zone/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { CategoryMetaType } from "@/lib/types/component";
import { DownloadIcon } from "lucide-react";

export const category: CategoryMetaType = {
name: "Use DropZone",
description: "A hook to handle file drop events",
icon: DownloadIcon,
latestUpdateDate: new Date("2025-02-08"),
};

export default category;
8 changes: 8 additions & 0 deletions packages/ui/cuicui/hooks/use-drop-zone/hook/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ComponentMetaType } from "@/lib/types/component";

export const component: ComponentMetaType = {
name: "Use DropZone",
description: "A hook to handle file drop events",
};

export default component;
37 changes: 37 additions & 0 deletions packages/ui/cuicui/hooks/use-drop-zone/hook/drop-zone.variant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";
import { useDropZone } from "@/cuicui/hooks/use-drop-zone";
import { cn } from "@/cuicui/utils/cn";
import { useRef } from "react";

export const DropZoneComponent = () => {
const dropZoneRef = useRef<HTMLDivElement>(null);
const { files, isOverDropZone } = useDropZone(dropZoneRef, {
onDrop: (files) => {
console.log("Dropped files:", files);
},
});

return (
<div>
<div
ref={dropZoneRef}
className={cn(
"border-2 p-8 rounded-2xl border-dashed",
isOverDropZone
? "border-blue-500 bg-neutral-400/0"
: "border-neutral-500 bg-neutral-400/15",
)}
>
Drop files here
{files && <div>Dropped {files.length} files</div>}
</div>
<p>Here is the filenames dropped:</p>
<ul>
{files?.map((file) => (
<li key={file.name}>{file.name}</li>
))}
</ul>
</div>
);
};
export default DropZoneComponent;
Loading

0 comments on commit 3bfbaec

Please sign in to comment.