diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml index ff340bac..d30ffbb0 100644 --- a/.github/workflows/firebase-hosting-merge.yml +++ b/.github/workflows/firebase-hosting-merge.yml @@ -3,6 +3,8 @@ on: push: branches: - main +env: + HOSTING_ENV: ${{ github.ref == 'refs/heads/main' && vars.HOSTING_ENV_PROD || vars.HOSTING_ENV_DEV }} jobs: code_check: name: Code check @@ -19,8 +21,8 @@ jobs: cache-dependency-path: | hosting/package-lock.json - - name: Apply formatting and linting - run: npm ci && npm run lint:fix && npm run format + - name: Run code checking and formatting + run: npm ci && npm run codecheck working-directory: ./hosting - name: Ensure code is unchanged @@ -53,6 +55,9 @@ jobs: restore-keys: | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}- + - name: Set hosting environment + run: echo $HOSTING_ENV > hosting/.env + - name: Build application run: npm ci && npm run build working-directory: ./hosting diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml index 9c7296a0..a08c5cf7 100644 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -4,6 +4,8 @@ permissions: checks: write contents: read pull-requests: write +env: + HOSTING_ENV: ${{ vars.HOSTING_ENV_DEV }} jobs: code_check: name: Code check @@ -20,8 +22,8 @@ jobs: cache-dependency-path: | hosting/package-lock.json - - name: Apply formatting and linting - run: npm ci && npm run lint:fix && npm run format + - name: Run code checking and formatting + run: npm ci && npm run codecheck working-directory: ./hosting - name: Ensure code is unchanged @@ -55,6 +57,9 @@ jobs: restore-keys: | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}- + - name: Set hosting environment + run: echo $HOSTING_ENV > hosting/.env + - name: Build application run: npm ci && npm run build working-directory: ./hosting diff --git a/hosting/README.md b/hosting/README.md index 7cb0fb87..a26d818a 100644 --- a/hosting/README.md +++ b/hosting/README.md @@ -1,123 +1,38 @@ -# TailAdmin Next.js - Free Next.js Tailwind Admin Dashboard Template +# Hosting -TailAdmin is a free and open-source admin dashboard template built on **Next.js and Tailwind CSS** providing developers with everything they need to create a feature-rich and data-driven: back-end, dashboard, or admin panel solution for any sort of web project. +## Configuration -[![tailwind nextjs admin template](https://github.com/TailAdmin/free-nextjs-admin-dashboard/blob/main/tailadmin-nextjs.jpg)](https://nextjs-demo.tailadmin.com/) +Make sure that you are configuring a `.env` file in the hosting folder. -With TailAdmin Next.js, you get access to all the necessary dashboard UI components, elements, and pages required to build a high-quality and complete dashboard or admin panel. Whether you're building a dashboard or admin panel for a complex web application or a simple website. - -TailAdmin utilizes the powerful features of **Next.js 13** and common features of Next.js such as server-side rendering (SSR), static site generation (SSG), and seamless API route integration. Combined with the advancements of **React 18** and the robustness of **TypeScript**, TailAdmin is the perfect solution to help get your project up and running quickly. - -### [✨ Visit Website](https://tailadmin.com/) - -### [🚀 PRO Demo](https://nextjs-demo.tailadmin.com/) - -### [🚀 FREE Demo](https://nextjs-free-demo.tailadmin.com/) - -### TailAdmin Next.js PRO vs TailAdmin Next.js FREE Comparison 📊 - -#### [TailAdmin Next.js PRO](https://nextjs-demo.tailadmin.com/) - -- 4 Unique Dashboards: Analytics, Ecommerce, Marketing, and CRM (More will be added) -- 120+ Dashboard UI Components -- 200+ Total UI Elements -- 45+ HTML Files -- All Essential Elements and Files -- Full Figma Design Source - As Shown on Demo - ---- - -#### [TailAdmin Next.js FREE](https://free-nextjs-demo.tailadmin.com/) - -- 1 Unique Dashboard -- 30+ Dashboard UI Components -- 50+ Total UI Elements -- 10+ HTML Files -- TypeScript Support -- Basic UI Kit Elements and Files -- Figma Design Source - Free Sample - ---- - -### [⬇️ Download Now](https://tailadmin.com/download) - -### [⚡ Get PRO Version](https://tailadmin.com/pricing) - -### [📄 Documentation/Installation](https://tailadmin.com/docs) - -### [🖌️ TailAdmin Figma Free Sample](https://www.figma.com/community/file/1214477970819985778) - -### [👉 TailAdmin HTML Version](https://github.com/TailAdmin/tailadmin-free-tailwind-dashboard-template) - -## Installation - -Here are the steps you need to follow to install the dependencies. - -1. Download and extract the template from Next.js Templates. - -2. After that **cd** into the template directory then run this command to install all the dependencies - -``` -npm install -``` - -or - -``` -yarn install -``` - -3. Now run this command to start the developement server +Take the configuration from your Firebase project web application settings. +There is a JSON configuration that you should add to the `.env` file as +displayed below. ``` -npm run dev +https://console.firebase.google.com/project//settings/general/ ``` -or +### `hosting/.env` +```dotenv +NEXT_PUBLIC_FIREBASE_API_KEY= +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= +NEXT_PUBLIC_FIREBASE_DATABASE_URL= +NEXT_PUBLIC_FIREBASE_PROJECT_ID= +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= +NEXT_PUBLIC_FIREBASE_APP_ID= ``` -yarn dev -``` - -## Free Admin Dashboard Template for Next.js Built-with Tailwind CSS, React 18 and TypeScript - -TailAdmin Next.js is a free dashboard template, which uses Tailwind CSS, is a great starting point for dashboard UI. This template uses the Next.js JavaScript framework and the easy-to-use Tailwind CSS framework. The Tailwind CSS and Next.js Dashboard Template comes with ready-made components like navigation menus, charts, tables, and forms. These components can be easily adjusted and added to any Next.js web application. - -TailAdmin for Next.js provides all essential Next.js + Tailwind CSS UI components that can be copied and pasted directly into your dashboard projects. The range of components includes charts, graphs, navbars, tabs, buttons, cards, tables, profiles, forms, modals, app pages, calendars, web app example templates, and more, all coded for Next.js React and styled using Tailwind CSS. - -If you're on the hunt for a top-quality Next.js-Tailwind Dashboard, Admin Panel Template, or UI Kit, TailAdmin is the perfect choice for you! - -### 📄 License -TailAdmin Next.js Free is 100% free and open-source; feel free to use it with your personal and commercial projects. +### Github actions -### 💜 Support +The `.env` file contents should be stored in a github varialbes as specified by +the github actions deployment scripts. -If you like the template, please star this repository to inspire the team to create more stuff like this and reach more users like you! +## Get up and running -## Update Logs +Start the application locally by simply running -### Version 1.3.1 - [Feb 12, 2024] - -#### Issues - -- **Issues 02:** Fix Misspelling issue [Default Layout/Layouts]. - -#### Enhancements - -- **Enhancement 01:** Update style.css - -### Version 1.3.0 - [Feb 05, 2024] - -#### Enhancements - -- **Enhancement 01:** Update Next.js into version 14 -- **Enhancement 02:** Integrate flatpickr in [Date Picker/Form Elements] -- **Enhancement 03:** Change color after select an option [Select Element/Form Elements]. -- **Enhancement 04:** Make it functional [Multiselect Dropdown/Form Elements]. -- **Enhancement 05:** Make best value editable [Pricing Table One/Pricing Table]. -- **Enhancement 06:** Add Default Layout Component and make App/Layout more clean and use it in every pages. - -### Version 0.1.0 - Initial Release - [Aug 3, 2023] - -- Initial release of TailAdmin Next. +```sh +npm run dev +``` diff --git a/hosting/package-lock.json b/hosting/package-lock.json index a270f3f1..64f1e380 100644 --- a/hosting/package-lock.json +++ b/hosting/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "apexcharts": "^3.45.2", + "dotenv": "^16.4.5", "firebase": "^10.12.0", "flatpickr": "^4.6.13", "jsvectormap": "^1.5.3", @@ -2316,6 +2317,17 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", diff --git a/hosting/package.json b/hosting/package.json index b08ddca6..bfbaa674 100644 --- a/hosting/package.json +++ b/hosting/package.json @@ -9,10 +9,11 @@ "lint": "next lint", "lint:fix": "next lint --fix", "prettier:fix": "prettier --write .", - "format": "prettier --write 'src/**/*.{js,ts,tsx,json,css,md}'" + "codecheck": "npm run lint:fix && npm run prettier:fix" }, "dependencies": { "apexcharts": "^3.45.2", + "dotenv": "^16.4.5", "firebase": "^10.12.0", "flatpickr": "^4.6.13", "jsvectormap": "^1.5.3", diff --git a/hosting/src/components/Sidebar/SidebarExpandableMenu.tsx b/hosting/src/components/Sidebar/SidebarExpandableMenu.tsx new file mode 100644 index 00000000..1876fdb1 --- /dev/null +++ b/hosting/src/components/Sidebar/SidebarExpandableMenu.tsx @@ -0,0 +1,65 @@ +import Link from "next/link"; +import {usePathname} from "next/navigation"; +import React, {useState} from "react"; +import SidebarLinkGroup from "./SidebarLinkGroup"; +import {CollapseExpandIcon} from "./icons/CollapseExpandIcon"; + +interface ExpandableMenuProps { + icon: React.ReactNode; + title: string; + menuItems: Array<{href: string; title: string}>; + isExpanded: boolean; +} + +export function SidebarExpandableMenu({icon, title, menuItems, isExpanded}: ExpandableMenuProps) { + const pathname = usePathname() ?? "/"; + const [isOpen, setIsOpen] = useState(isExpanded); + + const handleToggle = () => { + setIsOpen(!isOpen); + }; + + const isActive = menuItems.some((item) => pathname.includes(item.href)); + + return ( + + {(handleClick) => ( + + { + e.preventDefault(); + handleToggle(); + handleClick(); + }} + > + {icon} + {title} + + + {/* */} +
+
    + {menuItems.map((item) => ( +
  • + + {item.title} + +
  • + ))} +
+
+ {/* */} +
+ )} +
+ ); +} diff --git a/hosting/src/components/Sidebar/SidebarLinkGroup.tsx b/hosting/src/components/Sidebar/SidebarLinkGroup.tsx index 270b5f3f..3721f088 100644 --- a/hosting/src/components/Sidebar/SidebarLinkGroup.tsx +++ b/hosting/src/components/Sidebar/SidebarLinkGroup.tsx @@ -6,14 +6,14 @@ interface SidebarLinkGroupProps { activeCondition: boolean; } -const SidebarLinkGroup = ({children, activeCondition}: SidebarLinkGroupProps) => { +function SidebarLinkGroup({children, activeCondition}: SidebarLinkGroupProps) { const [open, setOpen] = useState(activeCondition); - const handleClick = () => { + function handleClick() { setOpen(!open); - }; + } return
  • {children(handleClick, open)}
  • ; -}; +} export default SidebarLinkGroup; diff --git a/hosting/src/components/Sidebar/SidebarMenuGroup.tsx b/hosting/src/components/Sidebar/SidebarMenuGroup.tsx new file mode 100644 index 00000000..f63c7e8e --- /dev/null +++ b/hosting/src/components/Sidebar/SidebarMenuGroup.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +interface SidebarMenuGroupProps { + title: string; + children: React.ReactNode; +} + +export function SidebarMenuGroup({title, children}: SidebarMenuGroupProps) { + return ( +
    +

    {title}

    +
      {children}
    +
    + ); +} diff --git a/hosting/src/components/Sidebar/SidebarMenuItem.tsx b/hosting/src/components/Sidebar/SidebarMenuItem.tsx new file mode 100644 index 00000000..22b479fd --- /dev/null +++ b/hosting/src/components/Sidebar/SidebarMenuItem.tsx @@ -0,0 +1,29 @@ +import Link from "next/link"; +import {usePathname} from "next/navigation"; +import React from "react"; + +interface SidebarMenuProps { + href: string; + icon: React.ReactNode; + title: string; +} + +export function SidebarMenuItem({href, icon, title}: SidebarMenuProps) { + const pathname = usePathname() ?? "/"; + + const isActive = pathname.includes(href); + + return ( +
  • + + {icon} + {title} + +
  • + ); +} diff --git a/hosting/src/components/Sidebar/icons/ArrowLeftIcon.tsx b/hosting/src/components/Sidebar/icons/ArrowLeftIcon.tsx new file mode 100644 index 00000000..dfb1e644 --- /dev/null +++ b/hosting/src/components/Sidebar/icons/ArrowLeftIcon.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +export function ArrowLeftIcon() { + return ( + + + + ); +} diff --git a/hosting/src/components/Sidebar/icons/AuthenticationIcon.tsx b/hosting/src/components/Sidebar/icons/AuthenticationIcon.tsx new file mode 100644 index 00000000..1b8ad158 --- /dev/null +++ b/hosting/src/components/Sidebar/icons/AuthenticationIcon.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +export function AuthenticationIcon() { + return ( + + + + + + + + + + + + ); +} diff --git a/hosting/src/components/Sidebar/icons/CalendarIcon.tsx b/hosting/src/components/Sidebar/icons/CalendarIcon.tsx new file mode 100644 index 00000000..609acd06 --- /dev/null +++ b/hosting/src/components/Sidebar/icons/CalendarIcon.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +export function CalendarIcon() { + return ( + + + + ); +} diff --git a/hosting/src/components/Sidebar/icons/ChartIcon.tsx b/hosting/src/components/Sidebar/icons/ChartIcon.tsx new file mode 100644 index 00000000..5413cba8 --- /dev/null +++ b/hosting/src/components/Sidebar/icons/ChartIcon.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +export function ChartIcon() { + return ( + + + + + + + + + + + + ); +} diff --git a/hosting/src/components/Sidebar/icons/CollapseExpandIcon.tsx b/hosting/src/components/Sidebar/icons/CollapseExpandIcon.tsx new file mode 100644 index 00000000..d1b83483 --- /dev/null +++ b/hosting/src/components/Sidebar/icons/CollapseExpandIcon.tsx @@ -0,0 +1,23 @@ +interface CollapseExpandIconProps { + isOpen: boolean; +} + +export function CollapseExpandIcon({isOpen}: CollapseExpandIconProps) { + return ( + + + + ); +} diff --git a/hosting/src/components/Sidebar/icons/DashboardIcon.tsx b/hosting/src/components/Sidebar/icons/DashboardIcon.tsx new file mode 100644 index 00000000..0d921e99 --- /dev/null +++ b/hosting/src/components/Sidebar/icons/DashboardIcon.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +export function DashboardIcon() { + return ( + + + + + + + ); +} diff --git a/hosting/src/components/Sidebar/icons/FormsIcon.tsx b/hosting/src/components/Sidebar/icons/FormsIcon.tsx new file mode 100644 index 00000000..73443fab --- /dev/null +++ b/hosting/src/components/Sidebar/icons/FormsIcon.tsx @@ -0,0 +1,35 @@ +import React from "react"; + +export function FormsIcon() { + return ( + + + + + + + + ); +} diff --git a/hosting/src/components/Sidebar/icons/ProfileIcon.tsx b/hosting/src/components/Sidebar/icons/ProfileIcon.tsx new file mode 100644 index 00000000..95196636 --- /dev/null +++ b/hosting/src/components/Sidebar/icons/ProfileIcon.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +export function ProfileIcon() { + return ( + + + + + ); +} diff --git a/hosting/src/components/Sidebar/icons/SettingsIcon.tsx b/hosting/src/components/Sidebar/icons/SettingsIcon.tsx new file mode 100644 index 00000000..91b49441 --- /dev/null +++ b/hosting/src/components/Sidebar/icons/SettingsIcon.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +export function SettingsIcon() { + return ( + + + + + + + + + + + + ); +} diff --git a/hosting/src/components/Sidebar/icons/TableIcon.tsx b/hosting/src/components/Sidebar/icons/TableIcon.tsx new file mode 100644 index 00000000..280eb690 --- /dev/null +++ b/hosting/src/components/Sidebar/icons/TableIcon.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +export function TableIcon() { + return ( + + + + + + + + + + + ); +} diff --git a/hosting/src/components/Sidebar/icons/UIElementsIcon.tsx b/hosting/src/components/Sidebar/icons/UIElementsIcon.tsx new file mode 100644 index 00000000..45756c76 --- /dev/null +++ b/hosting/src/components/Sidebar/icons/UIElementsIcon.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +export function UIElementsIcon() { + return ( + + + + + + + + + + + + + ); +} diff --git a/hosting/src/components/Sidebar/index.tsx b/hosting/src/components/Sidebar/index.tsx index 1ea35fb6..ca40604e 100644 --- a/hosting/src/components/Sidebar/index.tsx +++ b/hosting/src/components/Sidebar/index.tsx @@ -1,10 +1,17 @@ "use client"; -import React, {useEffect, useRef, useState} from "react"; -import {usePathname} from "next/navigation"; -import Link from "next/link"; import Image from "next/image"; -import SidebarLinkGroup from "@/components/Sidebar/SidebarLinkGroup"; +import Link from "next/link"; +import {usePathname} from "next/navigation"; +import {useEffect, useRef, useState} from "react"; +import {useTanamDocumentTypes} from "../../hooks/useTanamDocumentTypes"; +import {SidebarExpandableMenu} from "./SidebarExpandableMenu"; +import {SidebarMenuGroup} from "./SidebarMenuGroup"; +import {SidebarMenuItem} from "./SidebarMenuItem"; +import {DashboardIcon} from "./icons/DashboardIcon"; +import {FormsIcon} from "./icons/FormsIcon"; +import {ProfileIcon} from "./icons/ProfileIcon"; +import {SettingsIcon} from "./icons/SettingsIcon"; interface SidebarProps { sidebarOpen: boolean; @@ -12,16 +19,15 @@ interface SidebarProps { } const Sidebar = ({sidebarOpen, setSidebarOpen}: SidebarProps) => { - const pathname = usePathname(); - + const pathname = usePathname() ?? "/"; + const site = pathname.split("/")[1]; const trigger = useRef(null); const sidebar = useRef(null); + const {data: documentTypes} = useTanamDocumentTypes(site); const storedSidebarExpanded = "true"; - const [sidebarExpanded, setSidebarExpanded] = useState( - storedSidebarExpanded === null ? false : storedSidebarExpanded === "true", - ); + const [sidebarExpanded] = useState(storedSidebarExpanded === null ? false : storedSidebarExpanded === "true"); // close on click outside useEffect(() => { @@ -68,28 +74,6 @@ const Sidebar = ({sidebarOpen, setSidebarOpen}: SidebarProps) => { Logo

    Tanam

    - - {/* */} @@ -97,533 +81,20 @@ const Sidebar = ({sidebarOpen, setSidebarOpen}: SidebarProps) => { {/* */} {/* */} diff --git a/hosting/src/firebase.ts b/hosting/src/firebase.ts index e2112b76..eccf228d 100644 --- a/hosting/src/firebase.ts +++ b/hosting/src/firebase.ts @@ -2,13 +2,13 @@ import {initializeApp} from "firebase/app"; import {getFirestore} from "firebase/firestore"; const firebaseConfig = { - apiKey: "AIzaSyDGYwmKdPdUXkO6abAaLf6BHL8vqqvzdvQ", - authDomain: "tanam-testing.firebaseapp.com", - databaseURL: "https://tanam-testing.firebaseio.com", - projectId: "tanam-testing", - storageBucket: "tanam-testing.appspot.com", - messagingSenderId: "33159958289", - appId: "1:33159958289:web:1056a48a098332d8fd46b0", + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, }; export const firebaseApp = initializeApp(firebaseConfig); diff --git a/hosting/src/hooks/useTanamDocumentTypes.tsx b/hosting/src/hooks/useTanamDocumentTypes.tsx new file mode 100644 index 00000000..6cff31a5 --- /dev/null +++ b/hosting/src/hooks/useTanamDocumentTypes.tsx @@ -0,0 +1,45 @@ +import {useState, useEffect} from "react"; +import {firestore} from "@/firebase"; +import {TanamDocumentType} from "@/models/TanamDocumentType"; +import {collection, onSnapshot} from "firebase/firestore"; + +interface TanamDocumentTypeHook { + data: TanamDocumentType[]; + error: Error | null; +} + +/** + * Hook to get a stream of Tanam document types + * + * @param {string} site ID of the site + * @return {TanamDocumentTypeHook} Hook for document types subscription + */ +export function useTanamDocumentTypes(site: string): TanamDocumentTypeHook { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + const collectionRef = collection(firestore, `tanam/${site}/document-types`); + + const unsubscribe = onSnapshot( + collectionRef, + (snapshot) => { + const documentTypes = snapshot.docs.map((doc) => + TanamDocumentType.fromJson({ + id: doc.id, + ...doc.data(), + }), + ); + setData(documentTypes); + }, + (err) => { + setError(err); + }, + ); + + // Cleanup subscription on unmount + return () => unsubscribe(); + }, [site]); + + return {data, error}; +} diff --git a/hosting/src/models/TanamDocumentType.ts b/hosting/src/models/TanamDocumentType.ts new file mode 100644 index 00000000..62ede89c --- /dev/null +++ b/hosting/src/models/TanamDocumentType.ts @@ -0,0 +1,17 @@ +export class TanamDocumentType { + constructor( + public id: string, + public title: string, + ) {} + + static fromJson(json: any): TanamDocumentType { + return new TanamDocumentType(json.id, json.title); + } + + toJson(): any { + return { + id: this.id, + title: this.title, + }; + } +}