mirror of
https://github.com/khairul169/home-lab.git
synced 2025-04-28 08:39:34 +07:00
chore: initial commit
This commit is contained in:
commit
01e5ae1a19
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
web-build/
|
||||||
|
|
||||||
|
# Native
|
||||||
|
*.orig.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
|
||||||
|
# Metro
|
||||||
|
.metro-health-check*
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.*
|
||||||
|
yarn-debug.*
|
||||||
|
yarn-error.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
12
.vscode/settings.json
vendored
Normal file
12
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"tailwindCSS.classAttributes": [
|
||||||
|
"style",
|
||||||
|
"className",
|
||||||
|
],
|
||||||
|
"tailwindCSS.experimental.classRegex": [
|
||||||
|
"tw`([^`]*)",
|
||||||
|
["tw.style\\(([^)]*)\\)", "'([^']*)'"],
|
||||||
|
"cn\\(([^)]*)\\)",
|
||||||
|
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||||
|
]
|
||||||
|
}
|
30
app.json
Normal file
30
app.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "myapp",
|
||||||
|
"slug": "myapp",
|
||||||
|
"scheme": "myapp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/icon.png",
|
||||||
|
"userInterfaceStyle": "light",
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/splash.png",
|
||||||
|
"resizeMode": "contain",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"assetBundlePatterns": ["**/*"],
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/favicon.png"
|
||||||
|
},
|
||||||
|
"plugins": ["expo-router"]
|
||||||
|
}
|
||||||
|
}
|
BIN
assets/adaptive-icon.png
Normal file
BIN
assets/adaptive-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
assets/favicon.png
Normal file
BIN
assets/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
assets/splash.png
Normal file
BIN
assets/splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
18
babel.config.js
Normal file
18
babel.config.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module.exports = function(api) {
|
||||||
|
api.cache(true);
|
||||||
|
return {
|
||||||
|
presets: ['babel-preset-expo'],
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
'module-resolver',
|
||||||
|
{
|
||||||
|
root: ['./src'],
|
||||||
|
alias: {
|
||||||
|
'@/': './src',
|
||||||
|
'@ui': './src/components/ui',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "myapp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "expo-router/entry",
|
||||||
|
"scripts": {
|
||||||
|
"start": "expo start",
|
||||||
|
"android": "expo start --android",
|
||||||
|
"ios": "expo start --ios",
|
||||||
|
"web": "expo start --web"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/metro-runtime": "~3.1.3",
|
||||||
|
"@types/react": "~18.2.45",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"expo": "~50.0.11",
|
||||||
|
"expo-constants": "~15.4.5",
|
||||||
|
"expo-linking": "~6.2.2",
|
||||||
|
"expo-router": "~3.4.8",
|
||||||
|
"expo-status-bar": "~1.11.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-native": "0.73.4",
|
||||||
|
"react-native-safe-area-context": "4.8.2",
|
||||||
|
"react-native-screens": "~3.29.0",
|
||||||
|
"react-native-web": "~0.19.6",
|
||||||
|
"react-query": "^3.39.3",
|
||||||
|
"twrnc": "^4.1.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.20.0",
|
||||||
|
"babel-plugin-module-resolver": "^5.0.0"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
7814
pnpm-lock.yaml
generated
Normal file
7814
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
src/app/_layout.tsx
Normal file
23
src/app/_layout.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Slot } from "expo-router";
|
||||||
|
import { QueryClientProvider } from "react-query";
|
||||||
|
import queryClient from "@/lib/queryClient";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { StatusBar } from "expo-status-bar";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
|
const RootLayout = () => {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<StatusBar style="auto" />
|
||||||
|
<View style={cn("flex-1 bg-white", { paddingTop: insets.top })}>
|
||||||
|
<Slot />
|
||||||
|
</View>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RootLayout;
|
27
src/app/index.tsx
Normal file
27
src/app/index.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Text } from "react-native";
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import useAPI from "@/hooks/useAPI";
|
||||||
|
import { Ionicons } from "@ui/Icons";
|
||||||
|
import { VStack } from "@ui/Stack";
|
||||||
|
import Button from "@ui/Button";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const { data } = useAPI("/posts/1");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack className="gap-3 p-4">
|
||||||
|
<Text style={cn("text-2xl font-medium")}>App</Text>
|
||||||
|
<Text style={cn("w-full")}>{data?.body}</Text>
|
||||||
|
|
||||||
|
<Button label="Click me" />
|
||||||
|
<Button label="Click me" variant="secondary" />
|
||||||
|
<Button label="Click me" variant="ghost" />
|
||||||
|
<Button label="Click me" variant="outline" />
|
||||||
|
<Button label="Click me" variant="destructive" />
|
||||||
|
<Button icon={<Ionicons name="trash" />} size="icon" />
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
11
src/components/ui/Box.tsx
Normal file
11
src/components/ui/Box.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ComponentPropsWithClassName } from "@/types/components";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
type Props = ComponentPropsWithClassName<typeof View>;
|
||||||
|
|
||||||
|
const Box = ({ className, ...props }: Props) => {
|
||||||
|
return <View style={cn(className)} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Box;
|
96
src/components/ui/Button.tsx
Normal file
96
src/components/ui/Button.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
|
import { Pressable, Text } from "react-native";
|
||||||
|
import React from "react";
|
||||||
|
import Slot from "./Slot";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"flex flex-row items-center justify-center rounded-md",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary",
|
||||||
|
secondary: "bg-secondary",
|
||||||
|
destructive: "bg-red-500",
|
||||||
|
ghost: "",
|
||||||
|
link: "text-primary underline-offset-4",
|
||||||
|
outline: "border border-primary",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4",
|
||||||
|
sm: "h-8 px-2",
|
||||||
|
lg: "h-12 px-8",
|
||||||
|
icon: "h-10 w-10 px-0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttonTextVariants = cva("text-center font-medium", {
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "text-primary-foreground",
|
||||||
|
secondary: "text-secondary-foreground",
|
||||||
|
destructive: "text-white",
|
||||||
|
ghost: "text-primary",
|
||||||
|
link: "text-primary-foreground underline",
|
||||||
|
outline: "text-primary",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "text-base",
|
||||||
|
sm: "text-sm",
|
||||||
|
lg: "text-xl",
|
||||||
|
icon: "text-base",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ButtonProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof Pressable>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
label?: string;
|
||||||
|
labelClasses?: string;
|
||||||
|
className?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
function Button({
|
||||||
|
label,
|
||||||
|
labelClasses,
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
icon,
|
||||||
|
...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
const textStyles = cn(
|
||||||
|
buttonTextVariants({ variant, size, className: labelClasses })
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
style={({ pressed }) =>
|
||||||
|
cn(
|
||||||
|
buttonVariants({ variant, size, className }),
|
||||||
|
pressed ? "opacity-60" : "opacity-100"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon ? <Slot.View style={textStyles}>{icon}</Slot.View> : null}
|
||||||
|
|
||||||
|
{label ? <Text style={textStyles}>{label}</Text> : null}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { buttonVariants, buttonTextVariants };
|
||||||
|
|
||||||
|
export default Button;
|
3
src/components/ui/Icons.tsx
Normal file
3
src/components/ui/Icons.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
|
|
||||||
|
export { Ionicons };
|
206
src/components/ui/Slot.tsx
Normal file
206
src/components/ui/Slot.tsx
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
Image as RNImage,
|
||||||
|
Pressable as RNPressable,
|
||||||
|
Text as RNText,
|
||||||
|
View as RNView,
|
||||||
|
StyleSheet,
|
||||||
|
type PressableStateCallbackType,
|
||||||
|
type ImageProps as RNImageProps,
|
||||||
|
type ImageStyle as RNImageStyle,
|
||||||
|
type PressableProps as RNPressableprops,
|
||||||
|
type TextProps as RNTextProps,
|
||||||
|
type ViewProps as RNViewProps,
|
||||||
|
type StyleProp,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
const Pressable = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RNPressable>,
|
||||||
|
RNPressableprops
|
||||||
|
>((props, forwardedRef) => {
|
||||||
|
const { children, ...pressableslotProps } = props;
|
||||||
|
|
||||||
|
if (!React.isValidElement(children)) {
|
||||||
|
console.log("Slot.Pressable - Invalid asChild element", children);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement<
|
||||||
|
React.ComponentPropsWithoutRef<typeof RNPressable>,
|
||||||
|
React.ElementRef<typeof RNPressable>
|
||||||
|
>(isTextChildren(children) ? <></> : children, {
|
||||||
|
...mergeProps(pressableslotProps, children.props),
|
||||||
|
ref: forwardedRef
|
||||||
|
? composeRefs(forwardedRef, (children as any).ref)
|
||||||
|
: (children as any).ref,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Pressable.displayName = "SlotPressable";
|
||||||
|
|
||||||
|
const View = React.forwardRef<React.ElementRef<typeof RNView>, RNViewProps>(
|
||||||
|
(props, forwardedRef) => {
|
||||||
|
const { children, ...viewSlotProps } = props;
|
||||||
|
|
||||||
|
if (!React.isValidElement(children)) {
|
||||||
|
console.log("Slot.View - Invalid asChild element", children);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement<
|
||||||
|
React.ComponentPropsWithoutRef<typeof RNView>,
|
||||||
|
React.ElementRef<typeof RNView>
|
||||||
|
>(isTextChildren(children) ? <></> : children, {
|
||||||
|
...mergeProps(viewSlotProps, children.props),
|
||||||
|
ref: forwardedRef
|
||||||
|
? composeRefs(forwardedRef, (children as any).ref)
|
||||||
|
: (children as any).ref,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
View.displayName = "SlotView";
|
||||||
|
|
||||||
|
const Text = React.forwardRef<React.ElementRef<typeof RNText>, RNTextProps>(
|
||||||
|
(props, forwardedRef) => {
|
||||||
|
const { children, ...textSlotProps } = props;
|
||||||
|
|
||||||
|
if (!React.isValidElement(children)) {
|
||||||
|
console.log("Slot.Text - Invalid asChild element", children);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement<
|
||||||
|
React.ComponentPropsWithoutRef<typeof RNText>,
|
||||||
|
React.ElementRef<typeof RNText>
|
||||||
|
>(isTextChildren(children) ? <></> : children, {
|
||||||
|
...mergeProps(textSlotProps, children.props),
|
||||||
|
ref: forwardedRef
|
||||||
|
? composeRefs(forwardedRef, (children as any).ref)
|
||||||
|
: (children as any).ref,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Text.displayName = "SlotText";
|
||||||
|
|
||||||
|
type ImageSlotProps = RNImageProps & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Image = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RNImage>,
|
||||||
|
ImageSlotProps
|
||||||
|
>((props, forwardedRef) => {
|
||||||
|
const { children, ...imageSlotProps } = props;
|
||||||
|
|
||||||
|
if (!React.isValidElement(children)) {
|
||||||
|
console.log("Slot.Image - Invalid asChild element", children);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement<
|
||||||
|
React.ComponentPropsWithoutRef<typeof RNImage>,
|
||||||
|
React.ElementRef<typeof RNImage>
|
||||||
|
>(isTextChildren(children) ? <></> : children, {
|
||||||
|
...mergeProps(imageSlotProps, children.props),
|
||||||
|
ref: forwardedRef
|
||||||
|
? composeRefs(forwardedRef, (children as any).ref)
|
||||||
|
: (children as any).ref,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Image.displayName = "SlotImage";
|
||||||
|
|
||||||
|
const Slot = { Image, Pressable, Text, View };
|
||||||
|
export default Slot;
|
||||||
|
|
||||||
|
// This project uses code from WorkOS/Radix Primitives.
|
||||||
|
// The code is licensed under the MIT License.
|
||||||
|
// https://github.com/radix-ui/primitives/tree/main
|
||||||
|
|
||||||
|
function composeRefs<T>(...refs: (React.Ref<T> | undefined)[]) {
|
||||||
|
return (node: T) =>
|
||||||
|
refs.forEach((ref) => {
|
||||||
|
if (typeof ref === "function") {
|
||||||
|
ref(node);
|
||||||
|
} else if (ref != null) {
|
||||||
|
(ref as React.MutableRefObject<T>).current = node;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyProps = Record<string, any>;
|
||||||
|
|
||||||
|
function mergeProps(slotProps: AnyProps, childProps: AnyProps) {
|
||||||
|
// all child props should override
|
||||||
|
const overrideProps = { ...childProps };
|
||||||
|
|
||||||
|
for (const propName in childProps) {
|
||||||
|
const slotPropValue = slotProps[propName];
|
||||||
|
const childPropValue = childProps[propName];
|
||||||
|
|
||||||
|
const isHandler = /^on[A-Z]/.test(propName);
|
||||||
|
if (isHandler) {
|
||||||
|
// if the handler exists on both, we compose them
|
||||||
|
if (slotPropValue && childPropValue) {
|
||||||
|
overrideProps[propName] = (...args: unknown[]) => {
|
||||||
|
childPropValue(...args);
|
||||||
|
slotPropValue(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// but if it exists only on the slot, we use only this one
|
||||||
|
else if (slotPropValue) {
|
||||||
|
overrideProps[propName] = slotPropValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if it's `style`, we merge them
|
||||||
|
else if (propName === "style") {
|
||||||
|
overrideProps[propName] = combineStyles(slotPropValue, childPropValue);
|
||||||
|
} else if (propName === "className") {
|
||||||
|
overrideProps[propName] = [slotPropValue, childPropValue]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...slotProps, ...overrideProps };
|
||||||
|
}
|
||||||
|
|
||||||
|
type PressableStyle = RNPressableprops["style"];
|
||||||
|
type ImageStyle = StyleProp<RNImageStyle>;
|
||||||
|
type Style = PressableStyle | ImageStyle;
|
||||||
|
|
||||||
|
function combineStyles(slotStyle?: Style, childValue?: Style) {
|
||||||
|
if (typeof slotStyle === "function" && typeof childValue === "function") {
|
||||||
|
return (state: PressableStateCallbackType) => {
|
||||||
|
return StyleSheet.flatten([slotStyle(state), childValue(state)]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof slotStyle === "function") {
|
||||||
|
return (state: PressableStateCallbackType) => {
|
||||||
|
return childValue
|
||||||
|
? StyleSheet.flatten([slotStyle(state), childValue])
|
||||||
|
: slotStyle(state);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof childValue === "function") {
|
||||||
|
return (state: PressableStateCallbackType) => {
|
||||||
|
return slotStyle
|
||||||
|
? StyleSheet.flatten([slotStyle, childValue(state)])
|
||||||
|
: childValue(state);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return StyleSheet.flatten([slotStyle, childValue].filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTextChildren(
|
||||||
|
children:
|
||||||
|
| React.ReactNode
|
||||||
|
| ((state: PressableStateCallbackType) => React.ReactNode)
|
||||||
|
) {
|
||||||
|
return Array.isArray(children)
|
||||||
|
? children.every((child) => typeof child === "string")
|
||||||
|
: typeof children === "string";
|
||||||
|
}
|
28
src/components/ui/Stack.tsx
Normal file
28
src/components/ui/Stack.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Box from "./Box";
|
||||||
|
import { ComponentPropsWithClassName } from "@/types/components";
|
||||||
|
|
||||||
|
type StackProps = ComponentPropsWithClassName<typeof Box> & {
|
||||||
|
direction?: "row" | "column";
|
||||||
|
};
|
||||||
|
|
||||||
|
const Stack = ({ direction = "row", className, ...props }: StackProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={[
|
||||||
|
"flex",
|
||||||
|
direction === "row"
|
||||||
|
? "flex-row items-center"
|
||||||
|
: "flex-col items-stretch",
|
||||||
|
className,
|
||||||
|
]}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const HStack = (props: StackProps) => <Stack direction="row" {...props} />;
|
||||||
|
const VStack = (props: StackProps) => <Stack direction="column" {...props} />;
|
||||||
|
|
||||||
|
export { HStack, VStack };
|
||||||
|
export default Stack;
|
5
src/components/ui/index.tsx
Normal file
5
src/components/ui/index.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Box from "./Box";
|
||||||
|
import Button from "./Button";
|
||||||
|
import Stack, { HStack, VStack } from "./Stack";
|
||||||
|
|
||||||
|
export { Box, Button, Stack, HStack, VStack };
|
11
src/hooks/useAPI.ts
Normal file
11
src/hooks/useAPI.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import api, { APIParams } from "@/lib/api";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
|
||||||
|
const useAPI = <T = any>(url: string, params?: APIParams) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [url, params],
|
||||||
|
queryFn: async () => api<T>(url, { params }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAPI;
|
75
src/lib/api.ts
Normal file
75
src/lib/api.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { API_BASEURL } from "./constants";
|
||||||
|
|
||||||
|
export type APIParams = { [key: string]: any };
|
||||||
|
|
||||||
|
export type APIOptions = RequestInit & {
|
||||||
|
params?: APIParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = async <T>(url: string, options: APIOptions = {}): Promise<T> => {
|
||||||
|
const fullUrl = new URL(url, API_BASEURL);
|
||||||
|
|
||||||
|
if (options.params && Object.keys(options.params).length > 0) {
|
||||||
|
for (const [key, value] of Object.entries(options.params)) {
|
||||||
|
fullUrl.searchParams.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(fullUrl.toString(), options);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.headers.get("content-type")?.includes("application/json")) {
|
||||||
|
const data = (await response.json()) as T;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.text();
|
||||||
|
return data as T;
|
||||||
|
};
|
||||||
|
|
||||||
|
api.post = async <T>(url: string, body: any, options: APIOptions = {}) => {
|
||||||
|
return api<T>(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
api.patch = async <T>(url: string, body: any, options: APIOptions = {}) => {
|
||||||
|
return api<T>(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
api.put = async <T>(url: string, body: any, options: APIOptions = {}) => {
|
||||||
|
return api<T>(url, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
api.delete = async <T>(url: string, options: APIOptions = {}) => {
|
||||||
|
return api<T>(url, {
|
||||||
|
method: "DELETE",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export class APIError extends Error {
|
||||||
|
res: Response;
|
||||||
|
code: number;
|
||||||
|
|
||||||
|
constructor(res: Response) {
|
||||||
|
super(res.statusText);
|
||||||
|
this.res = res;
|
||||||
|
this.code = res.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api;
|
2
src/lib/constants.ts
Normal file
2
src/lib/constants.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// export const API_BASEURL = "http://localhost:8000";
|
||||||
|
export const API_BASEURL = "https://jsonplaceholder.typicode.com";
|
11
src/lib/queryClient.ts
Normal file
11
src/lib/queryClient.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { QueryClient } from "react-query";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default queryClient;
|
11
src/lib/utils.ts
Normal file
11
src/lib/utils.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ClassInput, create as createTwrnc } from "twrnc";
|
||||||
|
|
||||||
|
const tw = createTwrnc(require(`../../tailwind.config.js`));
|
||||||
|
|
||||||
|
export const cn = (...args: ClassInput[]) => {
|
||||||
|
if (Array.isArray(args[0])) {
|
||||||
|
return tw.style(...args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tw.style(...args);
|
||||||
|
};
|
6
src/types/components.ts
Normal file
6
src/types/components.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ComponentProps } from "react";
|
||||||
|
|
||||||
|
export type ComponentPropsWithClassName<T extends React.ElementType> =
|
||||||
|
ComponentProps<T> & {
|
||||||
|
className?: any;
|
||||||
|
};
|
40
tailwind.config.js
Normal file
40
tailwind.config.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./src/**/*.{jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "#6366F1",
|
||||||
|
50: "#FFFFFF",
|
||||||
|
100: "#F9F9FE",
|
||||||
|
200: "#D3D4FB",
|
||||||
|
300: "#AEAFF8",
|
||||||
|
400: "#888BF4",
|
||||||
|
500: "#6366F1",
|
||||||
|
600: "#3034EC",
|
||||||
|
700: "#1317D1",
|
||||||
|
800: "#0E119E",
|
||||||
|
900: "#0A0C6A",
|
||||||
|
950: "#070950",
|
||||||
|
},
|
||||||
|
'primary-foreground': '#fff',
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "#10B981",
|
||||||
|
50: "#8CF5D2",
|
||||||
|
100: "#79F3CB",
|
||||||
|
200: "#53F0BC",
|
||||||
|
300: "#2EEDAE",
|
||||||
|
400: "#13DF9B",
|
||||||
|
500: "#10B981",
|
||||||
|
600: "#0C855D",
|
||||||
|
700: "#075239",
|
||||||
|
800: "#031E15",
|
||||||
|
900: "#000000",
|
||||||
|
950: "#000000",
|
||||||
|
},
|
||||||
|
'secondary-foreground': '#161616',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@ui/*": ["./src/components/ui/*"],
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extends": "expo/tsconfig.base"
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user