├── src
├── types
│ └── nativewind-env.d.ts
├── styles
│ ├── global.css
│ └── colors.ts
├── assets
│ ├── expo.png
│ ├── banner.png
│ ├── javascript.png
│ ├── nativewind.png
│ ├── react-native.png
│ └── typescript.png
├── lib
│ └── utils.ts
├── components
│ ├── Title.tsx
│ ├── Switch.tsx
│ ├── Skills.tsx
│ ├── User.tsx
│ ├── Preferences.tsx
│ ├── Option.tsx
│ ├── Input.tsx
│ ├── Badge.tsx
│ ├── Checkbox.tsx
│ ├── Avatar.tsx
│ ├── Button.tsx
│ └── Toast.tsx
├── utils
│ └── skills.ts
└── app
│ └── Profile.tsx
├── assets
├── icon.png
├── splash.png
├── favicon.png
└── adaptive-icon.png
├── tsconfig.json
├── babel.config.js
├── metro.config.js
├── tailwind.config.js
├── App.tsx
├── .gitignore
├── app.json
└── package.json
/src/types/nativewind-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orodrigogo/nativecn-app/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orodrigogo/nativecn-app/HEAD/assets/splash.png
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orodrigogo/nativecn-app/HEAD/assets/favicon.png
--------------------------------------------------------------------------------
/src/assets/expo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orodrigogo/nativecn-app/HEAD/src/assets/expo.png
--------------------------------------------------------------------------------
/src/assets/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orodrigogo/nativecn-app/HEAD/src/assets/banner.png
--------------------------------------------------------------------------------
/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orodrigogo/nativecn-app/HEAD/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/src/assets/javascript.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orodrigogo/nativecn-app/HEAD/src/assets/javascript.png
--------------------------------------------------------------------------------
/src/assets/nativewind.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orodrigogo/nativecn-app/HEAD/src/assets/nativewind.png
--------------------------------------------------------------------------------
/src/assets/react-native.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orodrigogo/nativecn-app/HEAD/src/assets/react-native.png
--------------------------------------------------------------------------------
/src/assets/typescript.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orodrigogo/nativecn-app/HEAD/src/assets/typescript.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": ["./src/*"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/components/Title.tsx:
--------------------------------------------------------------------------------
1 | import { Text, TextProps } from "react-native"
2 |
3 | export function Title(props: TextProps) {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true)
3 | return {
4 | presets: [
5 | ["babel-preset-expo", { jsxImportSource: "nativewind" }],
6 | "nativewind/babel",
7 | ],
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig } = require("expo/metro-config")
2 | const { withNativeWind } = require("nativewind/metro")
3 |
4 | const config = getDefaultConfig(__dirname)
5 |
6 | module.exports = withNativeWind(config, { input: "./src/styles/global.css" })
7 |
--------------------------------------------------------------------------------
/src/styles/colors.ts:
--------------------------------------------------------------------------------
1 | export const colors = {
2 | white: "#FFFFFF",
3 | black: "#000000",
4 |
5 | green: {
6 | 400: "#70E1C1",
7 | 500: "#3ECF8F",
8 | },
9 |
10 | gray: {
11 | 400: "#707070",
12 | 500: "#2D2D2D",
13 | 600: "#232323",
14 | 700: "#1C1C1C",
15 | 800: "#161616",
16 | },
17 | }
18 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import { colors } from "./src/styles/colors"
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | content: ["./src/**/*.{js,jsx,ts,tsx}"],
6 | presets: [require("nativewind/preset")],
7 | theme: {
8 | extend: {
9 | colors,
10 | },
11 | },
12 | plugins: [],
13 | }
14 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/global.css"
2 |
3 | import { StatusBar } from "expo-status-bar"
4 | import { ToastProvider } from "@/components/Toast"
5 |
6 | import { Profile } from "@/app/Profile"
7 |
8 | export default function App() {
9 | return (
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/skills.ts:
--------------------------------------------------------------------------------
1 | export const SKILLS = [
2 | { name: "React Native", icon: require("@/assets/react-native.png") },
3 | { name: "Typescript", icon: require("@/assets/typescript.png") },
4 | { name: "Javascript", icon: require("@/assets/javascript.png") },
5 | { name: "Nativewind", icon: require("@/assets/nativewind.png") },
6 | { name: "Expo", icon: require("@/assets/expo.png") },
7 | ]
8 |
--------------------------------------------------------------------------------
/src/components/Switch.tsx:
--------------------------------------------------------------------------------
1 | import { Switch as NativeSwitch } from "react-native"
2 |
3 | import { colors } from "@/styles/colors"
4 |
5 | function Switch({
6 | ...props
7 | }: React.ComponentPropsWithoutRef) {
8 | return (
9 |
17 | )
18 | }
19 |
20 | export { Switch }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
--------------------------------------------------------------------------------
/src/components/Skills.tsx:
--------------------------------------------------------------------------------
1 | import { View } from "react-native"
2 |
3 | import { SKILLS } from "@/utils/skills"
4 | import { Title } from "@/components/Title"
5 | import { Badge } from "./Badge"
6 |
7 | export function Skills() {
8 | return (
9 |
10 | Skills
11 |
12 |
13 | {SKILLS.map((skill) => (
14 |
15 | ))}
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/User.tsx:
--------------------------------------------------------------------------------
1 | import { Text, View } from "react-native"
2 | import { Avatar, AvatarFallback, AvatarImage } from "./Avatar"
3 |
4 | export function User() {
5 | return (
6 |
7 |
8 |
9 | RG
10 |
11 |
12 |
13 | Rodrigo Gonçalves
14 |
15 |
16 | @orodrigogo
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "nativecn-app",
4 | "slug": "nativecn-app",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "assetBundlePatterns": [
15 | "**/*"
16 | ],
17 | "ios": {
18 | "supportsTablet": true
19 | },
20 | "android": {
21 | "adaptiveIcon": {
22 | "foregroundImage": "./assets/adaptive-icon.png",
23 | "backgroundColor": "#ffffff"
24 | }
25 | },
26 | "web": {
27 | "favicon": "./assets/favicon.png"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nativecn-app",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "class-variance-authority": "^0.7.0",
13 | "clsx": "^2.1.0",
14 | "expo": "~50.0.11",
15 | "expo-status-bar": "~1.11.1",
16 | "nativewind": "^4.0.36",
17 | "react": "18.2.0",
18 | "react-native": "0.73.4",
19 | "react-native-reanimated": "~3.6.2",
20 | "tailwind-merge": "^2.2.1"
21 | },
22 | "devDependencies": {
23 | "@babel/core": "^7.20.0",
24 | "@types/react": "~18.2.45",
25 | "tailwindcss": "^3.4.1",
26 | "typescript": "^5.1.3"
27 | },
28 | "private": true
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Preferences.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { View } from "react-native"
3 |
4 | import { Title } from "@/components/Title"
5 | import { Option } from "@/components/Option"
6 | import { Switch } from "@/components/Switch"
7 | import { Checkbox } from "@/components/Checkbox"
8 |
9 | export function Preferences() {
10 | const [isEnabled, setIsEnabled] = useState(false)
11 | return (
12 |
13 | Preferences
14 |
15 |
17 | Dark mode
18 |
19 |
20 |
21 |
23 | Public email
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Option.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react"
2 | import { Text, View, TextProps } from "react-native"
3 | import { MaterialIcons } from "@expo/vector-icons"
4 |
5 | import { colors } from "@/styles/colors"
6 |
7 | interface OptionProps {
8 | children: ReactNode
9 | }
10 |
11 | interface IconProps {
12 | icon: keyof typeof MaterialIcons.glyphMap
13 | }
14 |
15 | function Option({ children }: OptionProps) {
16 | return (
17 |
18 | {children}
19 |
20 | )
21 | }
22 |
23 | function Icon({ icon }: IconProps) {
24 | return
25 | }
26 |
27 | function Title({ ...rest }: TextProps) {
28 | return
29 | }
30 |
31 | Option.Title = Title
32 | Option.Icon = Icon
33 |
34 | export { Option }
35 |
--------------------------------------------------------------------------------
/src/components/Input.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from "react"
2 | import { Text, TextInput, View } from "react-native"
3 |
4 | import { cn } from "../lib/utils"
5 | import { colors } from "@/styles/colors"
6 |
7 | export interface InputProps
8 | extends React.ComponentPropsWithoutRef {
9 | label?: string
10 | labelClasses?: string
11 | inputClasses?: string
12 | }
13 | const Input = forwardRef, InputProps>(
14 | ({ className, label, labelClasses, inputClasses, ...props }, ref) => (
15 |
16 | {label && (
17 |
20 | {label}
21 |
22 | )}
23 |
31 |
32 | )
33 | )
34 |
35 | export { Input }
36 |
--------------------------------------------------------------------------------
/src/app/Profile.tsx:
--------------------------------------------------------------------------------
1 | import { Image, View, ScrollView } from "react-native"
2 |
3 | import { User } from "@/components/User"
4 | import { Input } from "@/components/Input"
5 | import { Button } from "@/components/Button"
6 | import { Skills } from "@/components/Skills"
7 | import { useToast } from "@/components/Toast"
8 | import { Preferences } from "@/components/Preferences"
9 |
10 | export function Profile() {
11 | const { toast } = useToast()
12 |
13 | return (
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Badge.tsx:
--------------------------------------------------------------------------------
1 | import { type VariantProps, cva } from "class-variance-authority"
2 | import { ImageProps, Text, View, Image } from "react-native"
3 |
4 | import { cn } from "../lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "flex flex-row items-center px-4 py-1 text-xs gap-1",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-gray-500 rounded-full",
12 | secondary: "bg-none border border-gray-500 rounded",
13 | },
14 | },
15 | defaultVariants: {
16 | variant: "default",
17 | },
18 | }
19 | )
20 |
21 | const badgeTextVariants = cva("font-medium text-center text-xs", {
22 | variants: {
23 | variant: {
24 | default: "text-white",
25 | secondary: "text-white",
26 | },
27 | },
28 | defaultVariants: {
29 | variant: "default",
30 | },
31 | })
32 |
33 | export interface BadgeProps
34 | extends React.ComponentPropsWithoutRef,
35 | VariantProps {
36 | label: string
37 | labelClasses?: string
38 | icon: ImageProps
39 | }
40 | function Badge({
41 | label,
42 | labelClasses,
43 | className,
44 | variant,
45 | icon,
46 | ...props
47 | }: BadgeProps) {
48 | return (
49 |
50 |
51 |
52 | {label}
53 |
54 |
55 | )
56 | }
57 |
58 | export { Badge, badgeVariants }
59 |
--------------------------------------------------------------------------------
/src/components/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { Text, TouchableOpacity, View } from "react-native"
3 | import { MaterialIcons } from "@expo/vector-icons"
4 |
5 | import { colors } from "@/styles/colors"
6 |
7 | import { cn } from "../lib/utils"
8 |
9 | // TODO: make controlled (optional)
10 | interface CheckboxProps extends React.ComponentPropsWithoutRef {
11 | label?: string
12 | labelClasses?: string
13 | checkboxClasses?: string
14 | }
15 | function Checkbox({
16 | label,
17 | labelClasses,
18 | checkboxClasses,
19 | className,
20 | ...props
21 | }: CheckboxProps) {
22 | const [isChecked, setChecked] = useState(false)
23 |
24 | const toggleCheckbox = () => {
25 | setChecked((prev) => !prev)
26 | }
27 |
28 | return (
29 |
33 |
34 |
43 | {isChecked && (
44 |
45 | )}
46 |
47 |
48 | {label && (
49 | {label}
50 | )}
51 |
52 | )
53 | }
54 |
55 | export { Checkbox }
56 |
--------------------------------------------------------------------------------
/src/components/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef, useState } from "react"
2 | import { Image, Text, View } from "react-native"
3 |
4 | import { cn } from "../lib/utils"
5 |
6 | const Avatar = forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 | ))
19 | Avatar.displayName = "Avatar"
20 |
21 | const AvatarImage = forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => {
25 | const [hasError, setHasError] = useState(false)
26 |
27 | if (hasError) {
28 | return null
29 | }
30 | return (
31 | setHasError(true)}
34 | className={cn("aspect-square h-full w-full", className)}
35 | {...props}
36 | />
37 | )
38 | })
39 | AvatarImage.displayName = "AvatarImage"
40 |
41 | const AvatarFallback = forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef & { textClassname?: string }
44 | >(({ children, className, textClassname, ...props }, ref) => (
45 |
53 | {children}
54 |
55 | ))
56 | AvatarFallback.displayName = "AvatarFallback"
57 |
58 | export { Avatar, AvatarImage, AvatarFallback }
59 |
--------------------------------------------------------------------------------
/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { type VariantProps, cva } from "class-variance-authority"
2 | import { Text, TouchableOpacity } from "react-native"
3 |
4 | import { cn } from "../lib/utils"
5 |
6 | const buttonVariants = cva(
7 | "flex flex-row items-center justify-center rounded-md",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-green-500",
12 | secondary: "bg-secondary",
13 | destructive: "bg-destructive",
14 | ghost: "bg-slate-700",
15 | link: "text-primary underline-offset-4",
16 | },
17 | size: {
18 | default: "h-14 px-4",
19 | sm: "h-8 px-2",
20 | lg: "h-12 px-8",
21 | },
22 | },
23 | defaultVariants: {
24 | variant: "default",
25 | size: "default",
26 | },
27 | }
28 | )
29 |
30 | const buttonTextVariants = cva("text-center font-medium", {
31 | variants: {
32 | variant: {
33 | default: "text-black font-bold",
34 | secondary: "text-secondary-foreground",
35 | destructive: "text-destructive-foreground",
36 | ghost: "text-primary-foreground",
37 | link: "text-primary-foreground underline",
38 | },
39 | size: {
40 | default: "text-lg",
41 | sm: "text-sm",
42 | lg: "text-xl",
43 | },
44 | },
45 | defaultVariants: {
46 | variant: "default",
47 | size: "default",
48 | },
49 | })
50 |
51 | interface ButtonProps
52 | extends React.ComponentPropsWithoutRef,
53 | VariantProps {
54 | label: string
55 | labelClasses?: string
56 | }
57 | function Button({
58 | label,
59 | labelClasses,
60 | className,
61 | variant,
62 | size,
63 | ...props
64 | }: ButtonProps) {
65 | return (
66 |
70 |
75 | {label}
76 |
77 |
78 | )
79 | }
80 |
81 | export { Button, buttonVariants, buttonTextVariants }
82 |
--------------------------------------------------------------------------------
/src/components/Toast.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useRef, useState } from "react"
2 | import { Animated, Text, View } from "react-native"
3 |
4 | import { cn } from "../lib/utils"
5 |
6 | const toastVariants = {
7 | default: "bg-green-500",
8 | destructive: "bg-destructive",
9 | success: "bg-green-500",
10 | info: "bg-blue-500",
11 | }
12 |
13 | interface ToastProps {
14 | id: number
15 | message: string
16 | onHide: (id: number) => void
17 | variant?: keyof typeof toastVariants
18 | duration?: number
19 | showProgress?: boolean
20 | }
21 | function Toast({
22 | id,
23 | message,
24 | onHide,
25 | variant = "default",
26 | duration = 3000,
27 | showProgress = true,
28 | }: ToastProps) {
29 | const opacity = useRef(new Animated.Value(0)).current
30 | const progress = useRef(new Animated.Value(0)).current
31 |
32 | useEffect(() => {
33 | Animated.sequence([
34 | Animated.timing(opacity, {
35 | toValue: 1,
36 | duration: 500,
37 | useNativeDriver: true,
38 | }),
39 | Animated.timing(progress, {
40 | toValue: 1,
41 | duration: duration - 1000,
42 | useNativeDriver: false,
43 | }),
44 | Animated.timing(opacity, {
45 | toValue: 0,
46 | duration: 500,
47 | useNativeDriver: true,
48 | }),
49 | ]).start(() => onHide(id))
50 | }, [duration])
51 |
52 | return (
53 |
70 |
71 | {message}
72 |
73 | {showProgress && (
74 |
75 |
84 |
85 | )}
86 |
87 | )
88 | }
89 |
90 | type ToastVariant = keyof typeof toastVariants
91 |
92 | interface ToastMessage {
93 | id: number
94 | text: string
95 | variant: ToastVariant
96 | duration?: number
97 | position?: string
98 | showProgress?: boolean
99 | }
100 | interface ToastContextProps {
101 | toast: (
102 | message: string,
103 | variant?: keyof typeof toastVariants,
104 | duration?: number,
105 | position?: "top" | "bottom",
106 | showProgress?: boolean
107 | ) => void
108 | removeToast: (id: number) => void
109 | }
110 | const ToastContext = createContext(undefined)
111 |
112 | // TODO: refactor to pass position to Toast instead of ToastProvider
113 | function ToastProvider({
114 | children,
115 | position = "top",
116 | }: {
117 | children: React.ReactNode
118 | position?: "top" | "bottom"
119 | }) {
120 | const [messages, setMessages] = useState([])
121 |
122 | const toast: ToastContextProps["toast"] = (
123 | message: string,
124 | variant: ToastVariant = "default",
125 | duration: number = 3000,
126 | position: "top" | "bottom" = "top",
127 | showProgress: boolean = true
128 | ) => {
129 | setMessages((prev) => [
130 | ...prev,
131 | {
132 | id: Date.now(),
133 | text: message,
134 | variant,
135 | duration,
136 | position,
137 | showProgress,
138 | },
139 | ])
140 | }
141 |
142 | const removeToast = (id: number) => {
143 | setMessages((prev) => prev.filter((message) => message.id !== id))
144 | }
145 |
146 | return (
147 |
148 | {children}
149 |
155 | {messages.map((message) => (
156 |
165 | ))}
166 |
167 |
168 | )
169 | }
170 |
171 | function useToast() {
172 | const context = useContext(ToastContext)
173 | if (!context) {
174 | throw new Error("useToast must be used within ToastProvider")
175 | }
176 | return context
177 | }
178 |
179 | export { ToastProvider, ToastVariant, Toast, toastVariants, useToast }
180 |
--------------------------------------------------------------------------------