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