87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const PYODIDE_VERSION = "0.27.4";
2 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export function getTypeForColumn(
9 | schema: Record,
10 | fileName: string,
11 | columnName: string
12 | ): string | undefined {
13 | const fileSchema = schema[fileName];
14 | const column = fileSchema?.find((col) => col.name === columnName);
15 | return column?.type;
16 | }
17 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | };
6 |
7 | export default nextConfig;
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "analytics-data-wrangler",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@duckdb/duckdb-wasm": "^1.29.1-dev132.0",
13 | "@radix-ui/react-accordion": "^1.2.2",
14 | "@radix-ui/react-checkbox": "^1.1.3",
15 | "@radix-ui/react-collapsible": "^1.1.3",
16 | "@radix-ui/react-dialog": "^1.1.4",
17 | "@radix-ui/react-label": "^2.1.1",
18 | "@radix-ui/react-select": "^2.1.3",
19 | "@radix-ui/react-separator": "^1.1.2",
20 | "@radix-ui/react-slot": "^1.1.1",
21 | "@radix-ui/react-toast": "^1.2.6",
22 | "apache-arrow": "^18.1.0",
23 | "class-variance-authority": "^0.7.1",
24 | "clsx": "^2.1.1",
25 | "duckdb-wasm-kit": "^0.1.39",
26 | "lucide-react": "^0.468.0",
27 | "next": "^15.3.2",
28 | "pyodide": "^0.27.6",
29 | "react": "^19.1.0",
30 | "react-dom": "^19.1.0",
31 | "react-icons": "^5.5.0",
32 | "sql-formatter": "^15.6.2",
33 | "tailwind-merge": "^2.5.5",
34 | "tailwindcss-animate": "^1.0.7",
35 | "zustand": "^5.0.5"
36 | },
37 | "devDependencies": {
38 | "@eslint/eslintrc": "^3",
39 | "@types/node": "^20",
40 | "@types/react": "^19",
41 | "@types/react-dom": "^19",
42 | "eslint": "^9",
43 | "eslint-config-next": "^15.2.4",
44 | "postcss": "^8",
45 | "tailwindcss": "^3.4.1",
46 | "typescript": "^5"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/adpivot_snapshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilo-css/analytics-data-pivot/b7b34911b3fc1c79854764ba910b28d4cb0b1cd8/public/adpivot_snapshot.png
--------------------------------------------------------------------------------
/stores/useDuckDBStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { AsyncDuckDB, initializeDuckDb } from "duckdb-wasm-kit";
3 | import { DuckDBConfig } from "@duckdb/duckdb-wasm";
4 |
5 | type DuckDBStore = {
6 | db: AsyncDuckDB | null;
7 | loadingduckdb: boolean;
8 | errorduckdb: Error | null;
9 | initializeDuckDB: () => Promise;
10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 | runQuery: (db: AsyncDuckDB, sql: string) => Promise;
12 | };
13 |
14 | export const useDuckDBStore = create((set) => ({
15 | db: null,
16 | loadingduckdb: false,
17 | errorduckdb: null,
18 | initializeDuckDB: async () => {
19 | try {
20 | set({ loadingduckdb: true, errorduckdb: null });
21 |
22 | const config: DuckDBConfig = {
23 | query: {
24 | castBigIntToDouble: true,
25 | },
26 | };
27 |
28 | try {
29 | const duckDBInstance = await initializeDuckDb({ config, debug: false });
30 | set({ db: duckDBInstance, loadingduckdb: false });
31 | } catch (errorduckdb) {
32 | set({ errorduckdb: errorduckdb as Error, loadingduckdb: false });
33 | }
34 | } catch (errorduckdb) {
35 | set({ errorduckdb: errorduckdb as Error, loadingduckdb: false });
36 | }
37 | },
38 | runQuery: async (db, sql) => {
39 | const conn = await db.connect();
40 | const arrow = await conn.query(sql);
41 | await conn.close();
42 | return arrow;
43 | },
44 | }));
45 |
--------------------------------------------------------------------------------
/stores/useExcelStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface ExcelStore {
4 | result: boolean;
5 | setResult: (result: boolean) => void;
6 | }
7 |
8 | export const useExcelStore = create((set) => ({
9 | result: false,
10 | setResult: (result) => set({ result }),
11 | }));
12 |
--------------------------------------------------------------------------------
/stores/useFileStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | type FileStoreType = {
4 | files: File[];
5 | addFile: (file: File) => void;
6 | removeFile: (fileName: string) => void;
7 | };
8 |
9 | export const useFileStore = create((set) => ({
10 | files: [],
11 | addFile: (file: File) =>
12 | set((state) => ({
13 | files: state.files.some((f) => f.name === file.name)
14 | ? state.files
15 | : [...state.files, file],
16 | })),
17 | removeFile: (fileName: string) =>
18 | set((state) => ({
19 | files: state.files.filter((file) => file.name !== fileName),
20 | })),
21 | }));
22 |
--------------------------------------------------------------------------------
/stores/useLanguageStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface LanguageState {
4 | language: string;
5 | toggleLanguage: () => void;
6 | }
7 |
8 | export const useLanguageStore = create((set) => ({
9 | language: "English",
10 | toggleLanguage: () =>
11 | set((state) => ({
12 | language: state.language === "English" ? "Portuguese" : "English",
13 | })),
14 | }));
15 |
--------------------------------------------------------------------------------
/stores/usePivotStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | type rowType = {
4 | name: string;
5 | table: string;
6 | dateExtract?: "YEAR" | "MONTH" | "QUARTER";
7 | };
8 |
9 | type columnType = {
10 | name: string;
11 | table: string;
12 | dateExtract?: "YEAR" | "MONTH" | "QUARTER";
13 | };
14 |
15 | type aggregationType = {
16 | name?: string;
17 | table?: string;
18 | type?: "SUM" | "AVG" | "MIN" | "MAX";
19 | };
20 |
21 | export type filterType = {
22 | table: string;
23 | field: string;
24 | values: string[];
25 | dateExtract?: "YEAR" | "MONTH" | "QUARTER";
26 | };
27 |
28 | export type PivotState = {
29 | rows: rowType[];
30 | setRows: (table: string, rows: string[]) => void;
31 | addRow: (
32 | table: string,
33 | row: string,
34 | dateExtract?: "YEAR" | "MONTH" | "QUARTER"
35 | ) => void;
36 | clearRow: (table: string, row: string) => void;
37 | clearRows: () => void;
38 | clearFileRows: (table?: string) => void;
39 | columns: columnType[];
40 | setColumns: (table: string, columns: string[]) => void;
41 | addColumn: (
42 | table: string,
43 | column: string,
44 | dateExtract?: "YEAR" | "MONTH" | "QUARTER"
45 | ) => void;
46 | clearColumn: (table: string, column: string) => void;
47 | clearColumns: () => void;
48 | clearFileColumns: (table?: string) => void;
49 | aggregation: aggregationType;
50 | setAggregation: (
51 | table: string,
52 | aggregation: string,
53 | type: "SUM" | "AVG" | "MIN" | "MAX"
54 | ) => void;
55 | clearAggregation: () => void;
56 | clearFileAggregation: (table?: string) => void;
57 | filters: filterType[];
58 | addFilter: (
59 | table: string,
60 | field: string,
61 | values: string[],
62 | dateExtract?: "YEAR" | "MONTH" | "QUARTER"
63 | ) => void;
64 | clearFilter: (table: string, field: string) => void;
65 | clearFilters: () => void;
66 | clearFileFilters: (table?: string) => void;
67 | };
68 |
69 | export const usePivotStore = create((set) => ({
70 | rows: [],
71 | setRows: (table, rows) =>
72 | set((state) => ({
73 | rows: [
74 | ...state.rows,
75 | ...rows
76 | .filter(
77 | (row) =>
78 | !state.rows.some((r) => r.table === table && r.name === row)
79 | )
80 | .map((row) => ({ name: row, table })),
81 | ],
82 | })),
83 | addRow: (table, row, dateExtract) => {
84 | set((state) => {
85 | const fieldId = dateExtract ? `${dateExtract}(${row})` : row;
86 | if (state.rows.some((r) => r.table === table && r.name === fieldId)) {
87 | return { rows: state.rows };
88 | }
89 | return {
90 | rows: [...state.rows, { name: fieldId, table, dateExtract }],
91 | };
92 | });
93 | },
94 | clearRow: (table, row) => {
95 | set((state) => ({
96 | rows: state.rows.filter((r) => !(r.table === table && r.name === row)),
97 | }));
98 | },
99 | clearRows: () => set({ rows: [] }),
100 | clearFileRows: (table) =>
101 | set((state) => ({
102 | rows: table ? state.rows.filter((r) => r.table !== table) : [],
103 | })),
104 | columns: [],
105 | setColumns: (table, columns) =>
106 | set((state) => ({
107 | columns: [
108 | ...state.columns,
109 | ...columns
110 | .filter(
111 | (column) =>
112 | !state.columns.some((c) => c.table === table && c.name === column)
113 | )
114 | .map((column) => ({ name: column, table })),
115 | ],
116 | })),
117 | addColumn: (table, column, dateExtract) => {
118 | set((state) => {
119 | const fieldId = dateExtract ? `${dateExtract}(${column})` : column;
120 | if (state.columns.some((c) => c.table === table && c.name === fieldId)) {
121 | return { columns: state.columns };
122 | }
123 | return {
124 | columns: [...state.columns, { name: fieldId, table, dateExtract }],
125 | };
126 | });
127 | },
128 | clearColumn: (table, column) => {
129 | set((state) => ({
130 | columns: state.columns.filter(
131 | (c) => !(c.table === table && c.name === column)
132 | ),
133 | }));
134 | },
135 | clearColumns: () => set({ columns: [] }),
136 | clearFileColumns: (table) =>
137 | set((state) => ({
138 | columns: table ? state.columns.filter((c) => c.table !== table) : [],
139 | })),
140 | aggregation: {},
141 | setAggregation: (table, name, type) => {
142 | set(() => ({
143 | aggregation: {
144 | table: table,
145 | name: name,
146 | type: type,
147 | },
148 | }));
149 | },
150 | clearAggregation: () => set({ aggregation: {} }),
151 | clearFileAggregation: (table) =>
152 | set((state) => ({
153 | aggregation:
154 | table && state.aggregation.table === table ? {} : state.aggregation,
155 | })),
156 | filters: [],
157 | addFilter: (table, field, values, dateExtract) => {
158 | set((state) => {
159 | const fieldId = dateExtract ? `${dateExtract}(${field})` : field;
160 | const filteredFilters = state.filters.filter(
161 | (f) => !(f.table === table && f.field === fieldId)
162 | );
163 | return {
164 | filters: [
165 | ...filteredFilters,
166 | { table, field: fieldId, values, dateExtract },
167 | ],
168 | };
169 | });
170 | },
171 | clearFilter: (table, field) => {
172 | set((state) => ({
173 | filters: state.filters.filter(
174 | (f) => !(f.table === table && f.field === field)
175 | ),
176 | }));
177 | },
178 | clearFilters: () => set({ filters: [] }),
179 | clearFileFilters: (table) =>
180 | set((state) => ({
181 | filters: table ? state.filters.filter((f) => f.table !== table) : [],
182 | })),
183 | }));
184 |
--------------------------------------------------------------------------------
/stores/usePyodideStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import type { PyodideInterface } from "pyodide";
3 | import { PYODIDE_VERSION } from "@/lib/constants";
4 |
5 | type PyodideStore = {
6 | pyodide: PyodideInterface | null;
7 | loadingpyodide: boolean;
8 | errorpyodide: Error | null;
9 | initializePyodide: () => Promise;
10 | };
11 |
12 | export const usePyodideStore = create((set) => ({
13 | pyodide: null,
14 | loadingpyodide: false,
15 | errorpyodide: null,
16 | initializePyodide: async () => {
17 | try {
18 | set({ loadingpyodide: true, errorpyodide: null });
19 |
20 | // Load Pyodide script
21 | const script = document.createElement("script");
22 | script.src = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/pyodide.js`;
23 | script.async = true;
24 | document.body.appendChild(script);
25 |
26 | await new Promise((resolve) => {
27 | script.onload = async () => {
28 | try {
29 | // Initialize Pyodide
30 | const pyodideInstance = await (
31 | window as Window &
32 | typeof globalThis & {
33 | loadPyodide: (options: {
34 | indexURL: string;
35 | }) => Promise;
36 | }
37 | ).loadPyodide({
38 | indexURL: `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`,
39 | });
40 |
41 | // Install and import pandas
42 | await pyodideInstance.loadPackage("pandas");
43 | await pyodideInstance.loadPackage("Jinja2");
44 | await pyodideInstance.loadPackage("micropip");
45 | await pyodideInstance.runPythonAsync(`
46 | import pandas as pd
47 | import js
48 | import micropip
49 | await micropip.install('openpyxl')
50 | await micropip.install('pyarrow')
51 | `);
52 |
53 | set({ pyodide: pyodideInstance, loadingpyodide: false });
54 | resolve();
55 | } catch (errorpyodide) {
56 | set({ errorpyodide: errorpyodide as Error, loadingpyodide: false });
57 | }
58 | };
59 | });
60 | } catch (errorpyodide) {
61 | set({ errorpyodide: errorpyodide as Error, loadingpyodide: false });
62 | }
63 | },
64 | }));
65 |
--------------------------------------------------------------------------------
/stores/useRelationalStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | type RelationshipType = {
4 | primary_table: string;
5 | primary_key: string;
6 | foreign_table: string;
7 | foreign_key: string;
8 | };
9 |
10 | interface RelationalState {
11 | relationships: RelationshipType[];
12 | addRelationship: (relationship: RelationshipType) => void;
13 | removeRelationship: (index: number) => void;
14 | clearRelationships: () => void;
15 | }
16 |
17 | export const useRelationalStore = create((set) => ({
18 | relationships: [],
19 |
20 | addRelationship: (relationship: RelationshipType) =>
21 | set((state) => ({
22 | relationships: [...state.relationships, relationship],
23 | })),
24 |
25 | removeRelationship: (index: number) =>
26 | set((state) => ({
27 | relationships: state.relationships.filter((_, i) => i !== index),
28 | })),
29 |
30 | clearRelationships: () =>
31 | set(() => ({
32 | relationships: [],
33 | })),
34 | }));
35 |
--------------------------------------------------------------------------------
/stores/useTableStore.ts:
--------------------------------------------------------------------------------
1 | import { AsyncDuckDB } from "duckdb-wasm-kit";
2 | import { create } from "zustand";
3 | import * as duckdb from "@duckdb/duckdb-wasm";
4 |
5 | export type FieldsType = {
6 | name: string;
7 | type: string;
8 | };
9 |
10 | type TableState = {
11 | queryResults: Record;
12 | setQueryResults: (tableName: string, results: unknown[]) => void;
13 | clearResults: (tableName: string) => void;
14 | queryFields: Record;
15 | setQueryFields: (tableName: string, fields: FieldsType[]) => void;
16 | clearQueryFields: (tableName: string) => void;
17 | isLoadingFields: boolean;
18 | setQueryFieldsFromFiles: (
19 | files: File[],
20 | db: AsyncDuckDB,
21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
22 | runQuery: any
23 | ) => Promise;
24 | };
25 |
26 | export const useTableStore = create((set) => ({
27 | queryResults: {},
28 | setQueryResults: (tableName, results) =>
29 | set((state) => ({
30 | queryResults: { ...state.queryResults, [tableName]: results },
31 | })),
32 | clearResults: (tableName) =>
33 | set((state) => {
34 | const newResults = { ...state.queryResults };
35 | delete newResults[tableName];
36 | return { queryResults: newResults };
37 | }),
38 | queryFields: {},
39 | isLoadingFields: false,
40 | setQueryFields: (tableName, fields) => {
41 | set((state) => ({
42 | queryFields: { ...state.queryFields, [tableName]: fields },
43 | }));
44 | },
45 | setQueryFieldsFromFiles: async (files, db, runQuery) => {
46 | const store = useTableStore.getState();
47 | set({ isLoadingFields: true });
48 | try {
49 | for (const file of Object.values(files)) {
50 | await db.registerFileHandle(
51 | file.name,
52 | file,
53 | duckdb.DuckDBDataProtocol.BROWSER_FILEREADER,
54 | true
55 | );
56 | const query = `SELECT * FROM '${file.name}' LIMIT 1`;
57 | const result = await runQuery(db, query);
58 |
59 | const fields: FieldsType[] = result.schema.fields
60 | .filter(
61 | (field: { name: string; type: string }) =>
62 | field.name !== "__index_level_0__"
63 | )
64 | .map((field: { name: string; type: string }) => ({
65 | name: field.name,
66 | type: field.type.toString(),
67 | }));
68 |
69 | store.setQueryFields(file.name, fields);
70 | }
71 | } finally {
72 | set({ isLoadingFields: false });
73 | }
74 | },
75 | clearQueryFields: (tableName) =>
76 | set((state) => {
77 | const newFields = { ...state.queryFields };
78 | delete newFields[tableName];
79 | return { queryFields: newFields };
80 | }),
81 | }));
82 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import { fontFamily } from "tailwindcss/defaultTheme";
3 | import tailwindcssAnimate from "tailwindcss-animate";
4 |
5 | export default {
6 | darkMode: ["class"],
7 | content: [
8 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
9 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
10 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
11 | ],
12 | theme: {
13 | extend: {
14 | colors: {
15 | background: "hsl(var(--background))",
16 | foreground: "hsl(var(--foreground))",
17 | card: {
18 | DEFAULT: "hsl(var(--card))",
19 | foreground: "hsl(var(--card-foreground))",
20 | },
21 | popover: {
22 | DEFAULT: "hsl(var(--popover))",
23 | foreground: "hsl(var(--popover-foreground))",
24 | },
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | muted: {
34 | DEFAULT: "hsl(var(--muted))",
35 | foreground: "hsl(var(--muted-foreground))",
36 | },
37 | accent: {
38 | DEFAULT: "hsl(var(--accent))",
39 | foreground: "hsl(var(--accent-foreground))",
40 | },
41 | destructive: {
42 | DEFAULT: "hsl(var(--destructive))",
43 | foreground: "hsl(var(--destructive-foreground))",
44 | },
45 | border: "hsl(var(--border))",
46 | input: "hsl(var(--input))",
47 | ring: "hsl(var(--ring))",
48 | chart: {
49 | "1": "hsl(var(--chart-1))",
50 | "2": "hsl(var(--chart-2))",
51 | "3": "hsl(var(--chart-3))",
52 | "4": "hsl(var(--chart-4))",
53 | "5": "hsl(var(--chart-5))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: {
64 | height: "0",
65 | },
66 | to: {
67 | height: "var(--radix-accordion-content-height)",
68 | },
69 | },
70 | "accordion-up": {
71 | from: {
72 | height: "var(--radix-accordion-content-height)",
73 | },
74 | to: {
75 | height: "0",
76 | },
77 | },
78 | },
79 | animation: {
80 | "accordion-down": "accordion-down 0.2s ease-out",
81 | "accordion-up": "accordion-up 0.2s ease-out",
82 | },
83 | fontFamily: {
84 | sans: ["var(--font-sans)", ...fontFamily.sans],
85 | heading: ["var(--font-heading)", ...fontFamily.sans],
86 | },
87 | },
88 | },
89 | plugins: [tailwindcssAnimate],
90 | } satisfies Config;
91 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
|