mirror of
https://github.com/khairul169/home-lab.git
synced 2026-10-02 17:13:57 +07:00
chore: initial commit
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
|
||||
export { Ionicons };
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,5 @@
|
||||
import Box from "./Box";
|
||||
import Button from "./Button";
|
||||
import Stack, { HStack, VStack } from "./Stack";
|
||||
|
||||
export { Box, Button, Stack, HStack, VStack };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
// export const API_BASEURL = "http://localhost:8000";
|
||||
export const API_BASEURL = "https://jsonplaceholder.typicode.com";
|
||||
@@ -0,0 +1,11 @@
|
||||
import { QueryClient } from "react-query";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default queryClient;
|
||||
@@ -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);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
export type ComponentPropsWithClassName<T extends React.ElementType> =
|
||||
ComponentProps<T> & {
|
||||
className?: any;
|
||||
};
|
||||
Reference in New Issue
Block a user