Skip to content
Snippets Groups Projects
Commit 50e5dbab authored by Princelle Maxime's avatar Princelle Maxime :gay_pride_flag:
Browse files

Merge branch 'develop' into 'main'

Add CI deployment

See merge request !4
parents 3569e496 cd89c001
1 merge request!4Add CI deployment
Showing with 3891 additions and 3198 deletions
node_modules
\ No newline at end of file
kind: pipeline
type: docker
name: deploy
clone:
disable: true
steps:
- name: notif build start
image: appleboy/drone-discord
settings:
webhook_id:
from_secret: discord_webhook_id
webhook_token:
from_secret: discord_webhook_token
username: "Drone Princelle (ERP: rms)"
avatar_url: https://s3.princelle.org/share/drone-ci.png
message: >
🚀 Starting deployment of **{{ repo.name }}** repo (*ERP*)...
**Commit from {{commit.author}} on {{commit.branch}}:**
{{commit.message}}
{{ build.link }}
- name: pull
image: appleboy/drone-ssh
settings:
host: flash.princelle.org
username: mprincelle
port: 22
key:
from_secret: ci_key
script:
- cd /app/uni/erp/rms
- git fetch --all
- git reset --hard origin/main
- git pull
- name: configure
image: appleboy/drone-ssh
settings:
host: flash.princelle.org
username: mprincelle
port: 22
key:
from_secret: ci_key
script:
- cd /app/uni/erp/rms
- echo -e "REACT_APP_BACK_API_KEY=theapikey" > .env
- echo -e "\nREACT_APP_BACK_URL=back.erp.uni.princelle.org" >> .env
- echo -e "\nAPP_PORT=2002" >> .env
- name: build
image: appleboy/drone-ssh
settings:
host: flash.princelle.org
username: mprincelle
port: 22
key:
from_secret: ci_key
script:
- cd /app/uni/erp/rms
- docker-compose up -d --build
- name: notif deploy done
image: appleboy/drone-discord
settings:
webhook_id:
from_secret: discord_webhook_id
webhook_token:
from_secret: discord_webhook_token
username: "Drone Princelle (ERP: rms)"
avatar_url: https://s3.princelle.org/share/drone-ci.png
message: >
✅ Successfully deployed **{{ repo.name }}** repo (*ERP*) on Princelle Cloud!
**Commit from {{commit.author}} on {{commit.branch}}:**
{{commit.message}}
**App URL:** https://rms.erp.uni.princelle.org/
{{ build.link }}
when:
status:
- success
- name: notif deploy failure
image: appleboy/drone-discord
settings:
webhook_id:
from_secret: discord_webhook_id
webhook_token:
from_secret: discord_webhook_token
username: "Drone Princelle (ERP: rms)"
avatar_url: https://s3.princelle.org/share/drone-ci.png
message: >
⚠️ Error on deploying **{{ repo.name }}** repo (*ERP*) on Princelle Cloud...
**Commit from {{commit.author}} on {{commit.branch}}:**
{{commit.message}}
{{ build.link }}
when:
status:
- failure
trigger:
branch:
- master
......@@ -21,3 +21,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea
.vscode
\ No newline at end of file
FROM node:16-alpine as build
WORKDIR /app
COPY . .
# Install & build
RUN yarn
RUN yarn build
# Production environment
FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY --from=build /app/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
version: '3.3'
services:
app:
container_name: erp-rms
restart: unless-stopped
build: .
ports:
- "${APP_PORT:-8889}:80"
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
}
}
\ No newline at end of file
import React from 'react';
function App() {
import {
BrowserRouter as Router,
Routes,
Route
} from "react-router-dom";
import Sell from './pages/sell';
import Login from './pages/login';
import Main from './components/layout/main';
import { AuthProvider } from './auth/AuthProvider';
export default function App() {
return (
<div className="container mx-auto pt-5">
<h1 className="text-3xl">Hello, World !</h1>
</div>
<Router>
<div>
<AuthProvider>
<Routes>
<Route index element={<h1>Welcome!</h1>} />
<Route path="login" element={<Login/>} />
<Route path="dashboard" element={<Main />}>
<Route index element={<Sell />} />
<Route path="sell" element={<h1>Sell...</h1>} />
<Route path="stock" element={<h1>Stock...</h1>} />
<Route path="settings" element={<h1>Settings...</h1>} />
</Route>
<Route path="*" element={<h1>No match !</h1>} />
</Routes>
</AuthProvider>
</div>
</Router>
);
}
export default App;
import React from 'react';
interface AuthContextType {
user: any;
signin: (user: string, callback: VoidFunction) => void;
signout: (callback: VoidFunction) => void;
};
const AuthContext = React.createContext<AuthContextType>(null!);
export { AuthContext };
\ No newline at end of file
import React from 'react';
import { AuthContext } from './AuthContext';
import { bake_cookie, delete_cookie } from 'sfcookies';
const fakeAuthProvider = {
isAuthenticated: false,
signin(callback: VoidFunction) {
fakeAuthProvider.isAuthenticated = true;
setTimeout(callback, 100); // fake async
},
signout(callback: VoidFunction) {
fakeAuthProvider.isAuthenticated = false;
setTimeout(callback, 100);
},
};
function AuthProvider({ children }: { children: React.ReactNode }) {
let [user, setUser] = React.useState<any>(null);
let signin = (newUser: string, callback: VoidFunction) => {
return fakeAuthProvider.signin(() => {
setUser(newUser);
bake_cookie('user', newUser);
callback();
});
};
let signout = (callback: VoidFunction) => {
return fakeAuthProvider.signout(() => {
setUser(null);
delete_cookie('user');
callback();
});
};
let value = { user, signin, signout };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export { AuthProvider };
\ No newline at end of file
import {
Navigate,
useLocation
} from "react-router-dom";
import { useAuth } from "./useAuth";
function RequireAuth({ children }: { children: JSX.Element }) {
let auth = useAuth();
let location = useLocation();
if (!auth.user) {
// Redirect them to the /login page, but save the current location they were
// trying to go to when they were redirected. This allows us to send them
// along to that page after they login, which is a nicer user experience
// than dropping them off on the home page.
return <Navigate to="/login" state={{ from: location }} />;
}
return children;
}
export { RequireAuth };
\ No newline at end of file
import React from 'react';
import { AuthContext } from './AuthContext';
import { read_cookie } from 'sfcookies';
function useAuth() {
let auth = React.useContext(AuthContext);
if(!auth.user && read_cookie('user').length !== 0)
auth.user = read_cookie('user');
return auth;
}
export { useAuth };
\ No newline at end of file
import { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import {
CogIcon,
CurrencyEuroIcon,
HomeIcon,
ShoppingCartIcon,
XIcon
} from '@heroicons/react/outline';
import { Link } from 'react-router-dom';
const navigation = [
{ name: 'Accueil', href: '/dashboard', icon: HomeIcon },
{ name: 'Vendre', href: '/dashboard/sell', icon: CurrencyEuroIcon },
{ name: 'Gestion des stocks', href: '/dashboard/stock', icon: ShoppingCartIcon },
{ name: 'Réglages', href: '/dashboard/settings', icon: CogIcon },
];
function classNames(...classes: unknown[]) {
return classes.filter(Boolean).join(' ');
}
const Navbar = ({ sidebarOpen, setSidebarOpen, path }) => {
return (
<>
<Transition.Root show={sidebarOpen} as={Fragment}>
<Dialog as="div" className="fixed inset-0 flex z-40 md:hidden" onClose={setSidebarOpen}>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-600 bg-opacity-75" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<div className="relative flex-1 flex flex-col max-w-xs w-full pt-5 pb-4 bg-gray-800">
<Transition.Child
as={Fragment}
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute top-0 right-0 -mr-12 pt-2">
<button
type="button"
className="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
onClick={() => setSidebarOpen(false)}
>
<span className="sr-only">Close sidebar</span>
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
</Transition.Child>
<div className="flex-shrink-0 flex items-center px-4">
<img
className="h-8 w-auto"
src="https://tailwindui.com/img/logos/workflow-logo-indigo-500-mark-white-text.svg"
alt="Workflow"
/>
</div>
<div className="mt-5 flex-1 h-0 overflow-y-auto">
<nav className="px-2 space-y-1">
{navigation.map((item) => (
<Link
key={item.name}
to={item.href}
className={classNames(
item.href === path ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white',
'group flex items-center px-2 py-2 text-base font-medium rounded-md'
)}
>
<item.icon
className={classNames(
item.href === path ? 'text-gray-300' : 'text-gray-400 group-hover:text-gray-300',
'mr-4 flex-shrink-0 h-6 w-6'
)}
aria-hidden="true"
/>
{item.name}
</Link>
))}
</nav>
</div>
</div>
</Transition.Child>
<div className="flex-shrink-0 w-14" aria-hidden="true">
{/* Dummy element to force sidebar to shrink to fit close icon */}
</div>
</Dialog>
</Transition.Root>
{/* Static sidebar for desktop */}
<div className="hidden md:flex md:flex-shrink-0">
<div className="flex flex-col w-64">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex-1 flex flex-col min-h-0">
<div className="flex items-center h-16 flex-shrink-0 px-4 bg-gray-900">
<img
className="h-8 w-auto"
src="https://tailwindui.com/img/logos/workflow-logo-indigo-500-mark-white-text.svg"
alt="Workflow"
/>
</div>
<div className="flex-1 flex flex-col overflow-y-auto">
<nav className="flex-1 px-2 py-4 bg-gray-800 space-y-1">
{navigation.map((item) => (
<Link
key={`desktop_${item.name}`}
to={item.href}
className={classNames(
item.href === path ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white',
'group flex items-center px-2 py-2 text-sm font-medium rounded-md'
)}
>
<item.icon
className={classNames(
item.href === path ? 'text-gray-300' : 'text-gray-400 group-hover:text-gray-300',
'mr-3 flex-shrink-0 h-6 w-6'
)}
aria-hidden="true"
/>
{item.name}
</Link>
))}
</nav>
</div>
</div>
</div>
</div>
</>
);
};
export default Navbar;
\ No newline at end of file
import { Fragment, useState } from 'react';
import { Menu, Transition } from '@headlessui/react';
import {
UserIcon,
MenuAlt2Icon,
} from '@heroicons/react/outline';
import Navbar from '../core/navbar';
import { useAuth } from '../../auth/useAuth';
import { RequireAuth } from '../../auth/RequireAuth';
import { Outlet, useNavigate } from 'react-router-dom';
interface LayoutProps {
title?: string,
subTitle?: string,
children?: any
};
function classNames(...classes: unknown[]) {
return classes.filter(Boolean).join(' ');
}
const Main = ({ title, subTitle, children }: LayoutProps) => {
let auth = useAuth();
let navigate = useNavigate();
let path = window.location.pathname;
const userNavigation = [
{ name: 'Se déconnecter', action: () => {return auth.signout(() => navigate("/"))} },
];
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<RequireAuth>
<div className="h-screen flex overflow-hidden bg-gray-100">
<Navbar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} path={path} />
<div className="flex flex-col w-0 flex-1 overflow-hidden">
<div className="relative z-10 flex-shrink-0 flex h-16 bg-white shadow">
<button
type="button"
className="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 md:hidden"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<MenuAlt2Icon className="h-6 w-6" aria-hidden="true" />
</button>
<div className="flex-1 px-4 flex justify-between">
<div className="flex-1 flex w-full md:ml-0 items-center"><span className="text-xl font-semibold">Dashboard</span></div>
<div className="ml-4 flex items-center md:ml-6">
{/* Profile dropdown */}
<Menu as="div" className="ml-3 relative">
<div>
<Menu.Button className="max-w-xs bg-white flex items-center text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span className="sr-only">Open user menu</span>
<UserIcon className="h-8 w-8 rounded-full" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item key="username"><span className="block px-4 py-2 text-sm text-gray-700 font-semibold">Bienvenue {auth.user} !</span></Menu.Item>
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
{({ active }) => (
<button
onClick={() => {item.action()}}
className={classNames(active ? 'bg-gray-100' : '', 'block px-4 py-2 text-sm text-gray-700 w-full')}
>
{item.name}
</button>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
</div>
<main className="flex-1 relative overflow-y-auto focus:outline-none">
<div className="py-6">
{title && (
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 mb-4">
<h1 className="text-2xl font-semibold text-gray-900">{title}</h1>
</div>
)}
{subTitle && (
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 -mt-4 mb-4">
<h1 className="text-xl font-normal text-gray-600">{subTitle}</h1>
</div>
)}
<Outlet />
</div>
</main>
</div>
</div>
</RequireAuth>
);
};
export default Main;
\ No newline at end of file
import {
ShoppingCartIcon,
} from '@heroicons/react/outline';
const SellListItem = ({ product }) => {
return (
<li className="col-span-1 bg-white rounded-lg shadow divide-y divide-gray-200">
<div className="w-full flex items-center justify-between p-6 space-x-6">
<div className="flex-1 truncate">
<div className="flex items-center space-x-3">
<h3 className="text-gray-900 text-sm font-medium truncate">{product.name}</h3>
<span className="flex-shrink-0 inline-block px-2 py-0.5 text-green-800 text-xs font-medium bg-green-100 rounded-full">
{product.quantity}
</span>
</div>
<p className="mt-1 text-gray-500 text-sm truncate">{product.price}</p>
</div>
</div>
<div>
<div className="-mt-px flex divide-x divide-gray-200">
<div className="-ml-px w-0 flex-1 flex">
<a
href='/'
className="relative w-0 flex-1 inline-flex items-center justify-center py-4 text-sm text-gray-700 font-medium border border-transparent rounded-br-lg hover:text-gray-500"
>
<ShoppingCartIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
<span className="ml-3">Ajouter</span>
</a>
</div>
</div>
</div>
</li>
);
};
export default SellListItem;
\ No newline at end of file
import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../auth/useAuth';
export default function Login(props) {
let navigate = useNavigate();
let location = useLocation();
let auth = useAuth();
let from = location.state?.from?.pathname || "/";
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
let formData = new FormData(event.currentTarget);
let username = formData.get("email") as string;
auth.signin(username, () => {
// Send them back to the page they tried to visit when they were
// redirected to the login page. Use { replace: true } so we don't create
// another entry in the history stack for the login page. This means that
// when they get to the protected page and click the back button, they
// won't end up back on the login page, which is also really nice for the
// user experience.
navigate(from, { replace: true });
});
}
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<img
className="mx-auto h-12 w-auto"
src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg"
alt="Workflow"
/>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Se connecter au Back-Office
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<form className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10" onSubmit={handleSubmit}>
<div className="space-y-6">
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700"
>
Adresse email
</label>
<div className="mt-1">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700"
>
Mot de passe
</label>
<div className="mt-1">
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
</div>
<div>
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Se connecter
</button>
</div>
</div>
</form>
</div>
</div>
);
}
import React, { useState, useEffect } from 'react';
import { CollectionIcon, EmojiSadIcon } from '@heroicons/react/outline';
import SellListItem from "../components/sellListItem";
const products = [
{
name: "Coca Cola",
category: "Boissons",
price: "0,50€",
quantity: "x50",
},
{
name: "Fanta",
category: "Boissons",
price: "0,50€",
quantity: "x50",
},
{
name: "Ice Tea",
category: "Boissons",
price: "0,50€",
quantity: "x50",
},
{
name: "Eau",
category: "Boissons",
price: "0,50€",
quantity: "x50",
},
{
name: "Eau gazeuse",
category: "Boissons",
price: "0,50€",
quantity: "x50",
},
{
name: "Sandwich",
category: "Manger",
price: "2€",
quantity: "x30",
},
{
name: "Pizza",
category: "Manger",
price: "2€",
quantity: "x20",
},
{
name: "Baguettine",
category: "Manger",
price: "1,50€",
quantity: "x20",
},
{
name: "Wrap",
category: "Manger",
price: "1,50€",
quantity: "x10",
},
{
name: "Donut",
category: "Dessert",
price: "1€",
quantity: "x30",
},
];
const Sell = () => {
let [search, setSearch] = useState("");
let [productsList, setProductsList] = useState(products);
useEffect(() => {
// Filter products by name with the search term
setProductsList(products.filter(product => {
return product.name.toLowerCase().includes(search.toLowerCase());
}))
}, [search]);
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<div className="pb-5">
<label htmlFor="search" className="block text-sm font-medium text-gray-700">Recherche</label>
<div className="mt-1 relative flex items-center">
<input type="text" name="search" id="search" value={search} onChange={(e) => { setSearch(e.target.value) }} className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full pr-12 sm:text-sm border-gray-300 rounded-md" />
</div>
</div>
{productsList.length > 0
? [...Array.from(new Set(productsList.map(item => item.category)))].map((category) => (
<div key={category}>
<h1 className="text-lg font-medium py-2 pt-4">{category}</h1>
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-5">
{productsList.filter((p) => p.category === category).map((product) => (
<SellListItem product={product} key={product.name} />
))}
</ul>
</div>))
: <div className="text-center w-full mt-10">
<EmojiSadIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">Aucun produit trouvé !</h3>
<p className="mt-1 text-sm text-gray-500">Essayez de réduire vos critères de recherche.</p>
<div className="mt-6">
<button
type="button"
onClick={() => setSearch("")}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<CollectionIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
Afficher tout
</button>
</div>
</div>
}
</div>
);
};
export default Sell;
......@@ -8,5 +8,7 @@ module.exports = {
variants: {
extend: {},
},
plugins: [],
plugins: [
require('@tailwindcss/forms')
],
}
......@@ -18,7 +18,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"noImplicitAny": false
},
"include": [
"src"
......
This diff is collapsed.
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment