├── src ├── constants │ └── contants.ts ├── app │ ├── favicon.ico │ ├── admin │ │ ├── page.ts │ │ ├── categories │ │ │ ├── page.tsx │ │ │ ├── categories.types.ts │ │ │ ├── create-category.schema.ts │ │ │ ├── category-form.tsx │ │ │ └── page-component.tsx │ │ ├── orders │ │ │ ├── types.ts │ │ │ ├── page.tsx │ │ │ └── page-component.tsx │ │ ├── products │ │ │ ├── page.tsx │ │ │ ├── products.types.ts │ │ │ ├── schema.ts │ │ │ ├── product-table-row.tsx │ │ │ ├── page-component.tsx │ │ │ └── product-form.tsx │ │ ├── dashboard │ │ │ ├── page.tsx │ │ │ └── page-component.tsx │ │ └── layout.tsx │ ├── auth │ │ ├── layout.tsx │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── lib │ └── utils.ts ├── supabase │ ├── client.ts │ ├── server.ts │ ├── middleware.ts │ └── types.ts ├── components │ ├── render-mounted.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── badge.tsx │ │ ├── scroll-area.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── table.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ ├── form.tsx │ │ ├── select.tsx │ │ └── dropdown-menu.tsx │ ├── footer.tsx │ ├── header.tsx │ └── category.tsx ├── templates │ ├── render-mounted.tsx │ ├── footer.tsx │ ├── category-form.tsx │ ├── auth.tsx │ ├── product-table-row.tsx │ ├── header.tsx │ ├── category-table-row.tsx │ └── product-form.tsx ├── providers │ └── theme-provider.tsx ├── middleware.ts ├── actions │ ├── auth.ts │ ├── notifications.ts │ ├── orders.ts │ ├── products.ts │ └── categories.ts ├── styles │ └── globals.css └── supabase.types.ts ├── .eslintrc.json ├── .env.example ├── public ├── app-pics.png ├── apple.jpeg ├── google-play.png ├── vercel.svg └── next.svg ├── postcss.config.mjs ├── next.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md └── tailwind.config.ts /src/constants/contants.ts: -------------------------------------------------------------------------------- 1 | export const ADMIN = 'admin'; 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SUPABASE_URL= 2 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 3 | -------------------------------------------------------------------------------- /public/app-pics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laribright/gadgets-shop-admin/HEAD/public/app-pics.png -------------------------------------------------------------------------------- /public/apple.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laribright/gadgets-shop-admin/HEAD/public/apple.jpeg -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laribright/gadgets-shop-admin/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/google-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laribright/gadgets-shop-admin/HEAD/public/google-play.png -------------------------------------------------------------------------------- /src/app/admin/page.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | const Admin = () => { 4 | return redirect('/admin/dashboard'); 5 | }; 6 | 7 | export default Admin; 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from '@supabase/ssr'; 2 | 3 | import { Database } from '@/supabase/types'; 4 | 5 | export function createClient() { 6 | return createBrowserClient( 7 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 8 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/admin/categories/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCategoriesWithProducts } from '@/actions/categories'; 2 | import CategoryPageComponent from '@/app/admin/categories/page-component'; 3 | 4 | export default async function Categories() { 5 | const categories = await getCategoriesWithProducts(); 6 | 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/render-mounted.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { ReactNode, useEffect, useState } from 'react'; 4 | 5 | export const RenderMounted = ({ children }: { children: ReactNode }) => { 6 | const [mounted, setMounted] = useState(false); 7 | 8 | useEffect(() => setMounted(true), []); 9 | 10 | if (!mounted) return null; 11 | 12 | return <>{children}; 13 | }; 14 | -------------------------------------------------------------------------------- /src/templates/render-mounted.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { ReactNode, useEffect, useState } from 'react'; 4 | 5 | export const RenderMounted = ({ children }: { children: ReactNode }) => { 6 | const [mounted, setMounted] = useState(false); 7 | 8 | useEffect(() => setMounted(true), []); 9 | 10 | if (!mounted) return null; 11 | 12 | return <>{children}; 13 | }; 14 | -------------------------------------------------------------------------------- /src/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 5 | import { type ThemeProviderProps } from 'next-themes/dist/types'; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'tvpgriftsewiyitmmyci.supabase.co', 8 | }, 9 | { 10 | protocol: 'https', 11 | hostname: 'images.unsplash.com', 12 | }, 13 | ], 14 | }, 15 | }; 16 | 17 | export default nextConfig; 18 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/app/admin/orders/types.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@/supabase/server'; 2 | import { QueryData } from '@supabase/supabase-js'; 3 | 4 | const supabase = createClient(); 5 | 6 | const ordersWithProductsQuery = supabase 7 | .from('order') 8 | .select('*, order_items:order_item(*, product(*)), user(*)') 9 | .order('created_at', { ascending: false }); 10 | 11 | export type OrdersWithProducts = QueryData; 12 | -------------------------------------------------------------------------------- /src/app/admin/categories/categories.types.ts: -------------------------------------------------------------------------------- 1 | import { ProductWithCategory } from '@/app/admin/products/products.types'; 2 | 3 | export type Category = { 4 | created_at: string; 5 | id: number; 6 | imageUrl: string; 7 | name: string; 8 | slug: string; 9 | }; 10 | 11 | export type CategoryWithProducts = { 12 | created_at: string; 13 | id: number; 14 | imageUrl: string; 15 | name: string; 16 | products: ProductWithCategory[]; 17 | slug: string; 18 | }; 19 | 20 | export type CategoriesWithProductsResponse = CategoryWithProducts[]; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/app/admin/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import { getOrdersWithProducts } from '@/actions/orders'; 2 | import PageComponent from '@/app/admin/orders/page-component'; 3 | 4 | const Orders = async () => { 5 | const ordersWithProducts = await getOrdersWithProducts(); 6 | 7 | if (!ordersWithProducts) 8 | return ( 9 |
No orders found
10 | ); 11 | 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | 19 | export default Orders; 20 | -------------------------------------------------------------------------------- /src/app/admin/products/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCategoriesWithProducts } from '@/actions/categories'; 2 | import { getProductsWithCategories } from '@/actions/products'; 3 | import { ProductPageComponent } from '@/app/admin/products/page-component'; 4 | 5 | export default async function Products() { 6 | const categories = await getCategoriesWithProducts(); 7 | const productsWithCategories = await getProductsWithCategories(); 8 | 9 | return ( 10 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/admin/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { getMonthlyOrders } from '@/actions/orders'; 2 | import PageComponent from './page-component'; 3 | import { getCategoryData } from '@/actions/categories'; 4 | import { getLatestUsers } from '@/actions/auth'; 5 | 6 | const Dashboard = async () => { 7 | const monthlyOrders = await getMonthlyOrders(); 8 | const categoryData = await getCategoryData(); 9 | const latestUsers = await getLatestUsers(); 10 | 11 | return ( 12 | 17 | ); 18 | }; 19 | 20 | export default Dashboard; 21 | -------------------------------------------------------------------------------- /src/app/admin/products/products.types.ts: -------------------------------------------------------------------------------- 1 | import { Category } from '@/app/admin/categories/categories.types'; 2 | 3 | export type ProductWithCategory = { 4 | category: Category; 5 | created_at: string; 6 | heroImage: string; 7 | id: number; 8 | imagesUrl: string[]; 9 | maxQuantity: number; 10 | price: number | null; 11 | slug: string; 12 | title: string; 13 | }; 14 | 15 | export type ProductsWithCategoriesResponse = ProductWithCategory[]; 16 | 17 | export type UpdateProductSchema = { 18 | category: number; 19 | heroImage: string; 20 | imagesUrl: string[]; 21 | maxQuantity: number; 22 | price: number; 23 | slug: string; 24 | title: string; 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from 'next/server'; 2 | 3 | import { updateSession } from '@/supabase/middleware'; 4 | 5 | export async function middleware(request: NextRequest) { 6 | return await updateSession(request); 7 | } 8 | 9 | export const config = { 10 | matcher: [ 11 | /* 12 | * Match all request paths except for the ones starting with: 13 | * - _next/static (static files) 14 | * - _next/image (image optimization files) 15 | * - favicon.ico (favicon file) 16 | * Feel free to modify this pattern to include more paths. 17 | */ 18 | '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$|^/$).*)', 19 | ], 20 | }; 21 | 22 | // |^/$ is making the home page accessible to all users 23 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ADMIN } from '@/constants/contants'; 2 | import { createClient } from '@/supabase/server'; 3 | import { redirect } from 'next/navigation'; 4 | import { ReactNode } from 'react'; 5 | 6 | export default async function AuthLayout({ 7 | children, 8 | }: Readonly<{ 9 | children: ReactNode; 10 | }>) { 11 | const supabase = createClient(); 12 | 13 | const { data: authData } = await supabase.auth.getUser(); 14 | 15 | if (authData?.user) { 16 | const { data, error } = await supabase 17 | .from('users') 18 | .select('*') 19 | .eq('id', authData.user.id) 20 | .single(); 21 | 22 | if (error || !data) { 23 | console.log('Error fetching user data', error); 24 | return; 25 | } 26 | 27 | if (data.type === ADMIN) return redirect('/admin'); 28 | } 29 | 30 | return <>{children}; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export const Footer = () => ( 4 |
5 |
6 | 7 | codewithlari 8 | 9 | 23 |
24 |
25 | ); 26 | -------------------------------------------------------------------------------- /src/templates/footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export const Footer = () => ( 4 |
5 |
6 | 7 | codewithlari 8 | 9 | 23 |
24 |
25 | ); 26 | -------------------------------------------------------------------------------- /src/supabase/server.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient } from '@supabase/ssr'; 2 | import { cookies } from 'next/headers'; 3 | 4 | import { Database } from '@/supabase/types'; 5 | 6 | export function createClient() { 7 | const cookieStore = cookies(); 8 | 9 | return createServerClient( 10 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 11 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 12 | { 13 | cookies: { 14 | getAll() { 15 | return cookieStore.getAll(); 16 | }, 17 | setAll(cookiesToSet) { 18 | try { 19 | cookiesToSet.forEach(({ name, value, options }) => 20 | cookieStore.set(name, value, options) 21 | ); 22 | } catch { 23 | // The `setAll` method was called from a Server Component. 24 | // This can be ignored if you have middleware refreshing 25 | // user sessions. 26 | } 27 | }, 28 | }, 29 | } 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import { Toaster } from '@/components/ui/sonner'; 4 | 5 | import '@/styles/globals.css'; 6 | import { ThemeProvider } from '@/providers/theme-provider'; 7 | 8 | const inter = Inter({ subsets: ['latin'] }); 9 | 10 | export const metadata: Metadata = { 11 | title: 'codewithlari rn-shop', 12 | description: 'React Native Shop - codewithlari', 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 23 | 29 |
{children}
30 | 31 |
32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner" 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 28 | ) 29 | } 30 | 31 | export { Toaster } 32 | -------------------------------------------------------------------------------- /src/actions/auth.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { createClient } from '@/supabase/server'; 4 | 5 | export const authenticate = async (email: string, password: string) => { 6 | const supabase = createClient(); 7 | try { 8 | const { error } = await supabase.auth.signInWithPassword({ 9 | email, 10 | password, 11 | }); 12 | 13 | if (error) throw error; 14 | } catch (error) { 15 | console.log('AUTHENTICATION ERROR', error); 16 | throw error; 17 | } 18 | }; 19 | 20 | export const getLatestUsers = async () => { 21 | const supabase = createClient(); 22 | const { data, error } = await supabase 23 | .from('users') 24 | .select('id, email, created_at') 25 | .order('created_at', { ascending: false }) 26 | .limit(5); 27 | 28 | if (error) throw new Error(`Error fetching latest users: ${error.message}`); 29 | 30 | return data.map( 31 | (user: { id: string; email: string; created_at: string | null }) => ({ 32 | id: user.id, 33 | email: user.email, 34 | date: user.created_at, 35 | }) 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/app/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from '@/components/footer'; 2 | import { Header } from '@/components/header'; 3 | import { RenderMounted } from '@/components/render-mounted'; 4 | import { ADMIN } from '@/constants/contants'; 5 | import { createClient } from '@/supabase/server'; 6 | import { redirect } from 'next/navigation'; 7 | import { ReactNode } from 'react'; 8 | 9 | export default async function AdminLayout({ 10 | children, 11 | }: Readonly<{ 12 | children: ReactNode; 13 | }>) { 14 | const supabase = createClient(); 15 | 16 | const { data: authData } = await supabase.auth.getUser(); 17 | 18 | if (authData?.user) { 19 | const { data, error } = await supabase 20 | .from('users') 21 | .select('*') 22 | .eq('id', authData.user.id) 23 | .single(); 24 | 25 | if (error || !data) { 26 | console.log('Error fetching user data', error); 27 | return; 28 | } 29 | 30 | if (data.type === ADMIN) return redirect('/'); 31 | } 32 | 33 | return ( 34 | 35 |
36 |
{children}
37 |