refont back express
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -40,3 +40,5 @@ testem.log
|
|||||||
# System files
|
# System files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
/backend/package-lock.json
|
||||||
|
/backend/node_modules
|
||||||
|
|||||||
7
backend/.env
Normal file
7
backend/.env
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALLOWED_ORIGIN=http://localhost:4300
|
||||||
|
PORT=3333
|
||||||
|
JWT_SECRET=d1bcf750e6246f1d75a2b32e3ff9ac7a8507b1d27462fc3223b966a86e3ef28762efcfc90a50e20b1888b1c8d7ce107260784b6d3ac9cb3662f499a7b272b1d926e3e0232de3581f25cde6e6da3dd0f62ee00fbe8bbfc3425c5abc0ad3b53f9b9c75875fe57629c9f1fe01ffb280d5605a54ab90ca15ae4cb6c43e298448e95bffd55582a16d18867d3c5db1cb316ba5fc9dfbdde1d8ef523a2d35f425aacd42286058801e79dc0b7c3fd3f9430ef3696e0fbcef5f28ba12ba3e38ee7e9b8f79d9ae51fb81f13528e9008d917d5be6145f3ab9a621dc89aef1f4df09bcdcce9f109f4f792623061ad1cfe541097fdf3695cfb72673ece58db49894d25f486e99
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_USER=eni
|
||||||
|
DB_PASSWORD=gkAAUlq2e)0*ROLO
|
||||||
|
DB_DATABASE=eni
|
||||||
16
backend/config/cors.js
Normal file
16
backend/config/cors.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
const envFile = `.env`;
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), envFile), override: true });
|
||||||
|
|
||||||
|
const allowedOrigin = process.env.ALLOWED_ORIGIN || 'http://localhost:4200';
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
origin: allowedOrigin,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
credentials: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default corsOptions;
|
||||||
13
backend/config/db.js
Normal file
13
backend/config/db.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import mariadb from 'mariadb';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = mariadb.createPool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_DATABASE
|
||||||
|
});
|
||||||
|
|
||||||
|
export default pool;
|
||||||
6
backend/controllers/apiController.js
Normal file
6
backend/controllers/apiController.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default function getConnect(req, res) {
|
||||||
|
res.json({
|
||||||
|
message: 'Connexion : OK',
|
||||||
|
status: true
|
||||||
|
});
|
||||||
|
};
|
||||||
143
backend/controllers/authController.js
Normal file
143
backend/controllers/authController.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import pool from "../config/db.js";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
// Création d'un utilisateur
|
||||||
|
export const register = async (req, res) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await pool.getConnection();
|
||||||
|
const { email, firstname, lastname, password_hash } = req.body;
|
||||||
|
if (!email || !firstname || !lastname || !password_hash) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: 'Tout les champs sont requis !',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const existingUsers = await conn.query(
|
||||||
|
'SELECT * FROM users WHERE email = ?',
|
||||||
|
[email]
|
||||||
|
);
|
||||||
|
if (existingUsers.length > 0) {
|
||||||
|
return res.status(409).json({
|
||||||
|
message: 'L\'utilisateur existe déjà !',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Hash du mot de passe
|
||||||
|
const hashedPassword = await bcrypt.hash(password_hash, 10);
|
||||||
|
const result = await conn.query(
|
||||||
|
'INSERT INTO users (email, firstname, lastname, password_hash) VALUES (?, ?, ?, ?, ?)',
|
||||||
|
[email, firstname, lastname, hashedPassword]
|
||||||
|
);
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'L\'utilisateur à bien été enregistré !',
|
||||||
|
status: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur interne :', err);
|
||||||
|
return res.status(500).json({
|
||||||
|
message: 'Erreur interne du serveur.',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (conn) await conn.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connexion
|
||||||
|
export const login = async (req, res) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await pool.getConnection();
|
||||||
|
const { email, password } = req.body;
|
||||||
|
// Chercher l'utilisateur
|
||||||
|
const users = await conn.query(
|
||||||
|
'SELECT * FROM users WHERE email = ?',
|
||||||
|
[email]
|
||||||
|
);
|
||||||
|
if (users.length === 0) {
|
||||||
|
return res.status(401).json({
|
||||||
|
message: 'Identifiant incorrect',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const user = users[0];
|
||||||
|
// Verify password
|
||||||
|
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return res.status(401).json({
|
||||||
|
message: 'Identifiant incorrect',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Generate JWT token
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
process.env.JWT_SECRET || 'fallback_secret',
|
||||||
|
{ expiresIn: '12h' }
|
||||||
|
);
|
||||||
|
await conn.query(
|
||||||
|
'UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||||
|
[user.id]
|
||||||
|
);
|
||||||
|
await setLogs(email + " s'est connecté !")
|
||||||
|
res.cookie('jwt', token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
maxAge: 12 * 60 * 60 * 1000
|
||||||
|
});
|
||||||
|
res.json({
|
||||||
|
message: 'Connexion réussie',
|
||||||
|
status: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur interne :', err);
|
||||||
|
return res.status(500).json({
|
||||||
|
message: 'Erreur interne du serveur.',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (conn) await conn.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Déconnecter l'utilisateur
|
||||||
|
export const logout = (req, res) => {
|
||||||
|
res.clearCookie('jwt', {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
});
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Successfully logged out',
|
||||||
|
status: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vérification de la connexion sur la page pour guard angular
|
||||||
|
export const checkAuth = (req, res) => {
|
||||||
|
const token = req.cookies.jwt;
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({
|
||||||
|
message: 'Non identifié',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'identifié',
|
||||||
|
status: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur interne :', err);
|
||||||
|
return res.status(500).json({
|
||||||
|
message: 'Erreur interne du serveur.',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
24
backend/middleware/apiKeyMiddleware.js
Normal file
24
backend/middleware/apiKeyMiddleware.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export const apiKeyMiddleware = async (req, res, next) => {
|
||||||
|
const authHeader = req.header('Authorization');
|
||||||
|
if (!authHeader) {
|
||||||
|
return res.status(403).json({
|
||||||
|
message: 'Forbidden: No API Key provided',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (authHeader !== "9IgFg8cnUS4XJE7Q91A0XjrWnjbnBhdk98jcI6fV1n6NAEYz31SHicge8Vkq0bCGvfKsjylb19ouri6FFUeNC1PgPvwrNCC3G5jcz4PLInlFanzf47hCsBJw4IXuhNHC"){
|
||||||
|
return res.status(403).json({
|
||||||
|
message: 'Forbidden: Invalid API Key',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur interne :', err);
|
||||||
|
return res.status(500).json({
|
||||||
|
message: 'Erreur interne du serveur.',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
25
backend/middleware/tokenJWTMiddleware.js
Normal file
25
backend/middleware/tokenJWTMiddleware.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
export const verifyToken = async (req, res, next) => {
|
||||||
|
// Récupérer le token depuis le cookie 'jxwt'
|
||||||
|
const token = req.cookies['jwt'];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(403).json({
|
||||||
|
message: 'Token is required',
|
||||||
|
status: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le token
|
||||||
|
jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret', (err, decoded) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(401).json({
|
||||||
|
message: 'Invalid or expired token',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
req.user = decoded;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
24
backend/package.json
Normal file
24
backend/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "api_eni_angular",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "API ENI Angular",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"author": "Johan Leroy",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mariadb": "^3.4.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"url": "^0.11.4",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
backend/routes/apiRoutes.js
Normal file
53
backend/routes/apiRoutes.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import express from "express";
|
||||||
|
const router = express.Router();
|
||||||
|
import apiController from "../controllers/apiController.js";
|
||||||
|
import {
|
||||||
|
register,
|
||||||
|
login,
|
||||||
|
checkAuth,
|
||||||
|
logout,
|
||||||
|
} from "../controllers/authController.js";
|
||||||
|
import {verifyToken} from "../middleware/tokenJWTMiddleware.js";
|
||||||
|
import multer from "multer";
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
router.get('', apiController);
|
||||||
|
|
||||||
|
// Upload
|
||||||
|
const generateFileName = (originalName) => {
|
||||||
|
const ext = path.extname(originalName);
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = now.getFullYear().toString() +
|
||||||
|
String(now.getMonth() + 1).padStart(2, '0') +
|
||||||
|
String(now.getDate()).padStart(2, '0') +
|
||||||
|
String(now.getHours()).padStart(2, '0') +
|
||||||
|
String(now.getMinutes()).padStart(2, '0') +
|
||||||
|
String(now.getSeconds()).padStart(2, '0');
|
||||||
|
const uuid = uuidv4();
|
||||||
|
return `${timestamp}_${uuid}${ext}`;
|
||||||
|
};
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, 'uploads/'); // Stockage des fichiers dans le dossier uploads/
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
cb(null, generateFileName(file.originalname));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
router.post('/upload', verifyToken, upload.single('file'), (req, res) => {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ message: 'Aucun fichier fourni' });
|
||||||
|
}
|
||||||
|
const filePath = `/uploads/${req.file.filename}`;
|
||||||
|
res.json({ filePath });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Authentification et utilisateur
|
||||||
|
router.post('/auth/register', register);
|
||||||
|
router.post('/auth/login', login);
|
||||||
|
router.post('/auth/logout', logout);
|
||||||
|
router.get('/auth/check-auth', verifyToken, checkAuth);
|
||||||
|
|
||||||
|
export default router;
|
||||||
82
backend/server.js
Normal file
82
backend/server.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import apiRoutes from './routes/apiRoutes.js';
|
||||||
|
import corsOptions from './config/cors.js'
|
||||||
|
import {apiKeyMiddleware} from './middleware/apiKeyMiddleware.js';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
import pool from "./config/db.js";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const envFile = `.env`;
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), envFile), override: true });
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Utiliser CORS
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
|
// Utilisation des cookies
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
// Middleware pour parser les corps de requêtes JSON
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Utilisation des fichiers/images
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||||
|
|
||||||
|
// Middleware pour vérifier la clé API (Authorization)
|
||||||
|
app.use('/', apiKeyMiddleware);
|
||||||
|
|
||||||
|
// Utiliser les routes API
|
||||||
|
app.use('/', apiRoutes);
|
||||||
|
|
||||||
|
// Middleware pour les routes non trouvées
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({
|
||||||
|
message: 'Forbidden: Invalid Request',
|
||||||
|
status: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test de conenxion à la base de donnée
|
||||||
|
const testDatabaseConnection = async () => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await pool.getConnection();
|
||||||
|
console.log('✅ Connexion à la base de données réussie ! 🎉');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Erreur de connexion à la base de données :', err.message);
|
||||||
|
throw new Error('⛔ Impossible de se connecter à la base de données.');
|
||||||
|
} finally {
|
||||||
|
if (conn) await conn.end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lancer le serveur
|
||||||
|
const startServer = async () => {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
console.log(`🚀 Lancement de l'API en cours... 🔧`);
|
||||||
|
await testDatabaseConnection();
|
||||||
|
app.listen(process.env.PORT, () => {
|
||||||
|
console.log(`📢 NODE_ENV = ${process.env.NODE_ENV} 🌍`);
|
||||||
|
console.log(`📢 Chargement du fichier : ${envFile} 📄`);
|
||||||
|
console.log(`🚀 API démarrée sur le port ${process.env.PORT} 🎯`);
|
||||||
|
console.log(`🌍 Allowed Origin : ${process.env.ALLOWED_ORIGIN} 🔗`);
|
||||||
|
console.log(`✅ L'API est lancée et prête à l'emploi ! 🎉`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Le serveur n’a pas pu démarrer :', err.message);
|
||||||
|
console.log('⏳ Nouvelle tentative de démarrage dans 5 secondes... 🔄');
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
startServer();
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve --port=4300",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test"
|
"test": "ng test"
|
||||||
|
|||||||
@@ -22,12 +22,6 @@
|
|||||||
<p class="block mr-auto font-sans text-base antialiased font-normal leading-relaxed text-blue-gray-900">
|
<p class="block mr-auto font-sans text-base antialiased font-normal leading-relaxed text-blue-gray-900">
|
||||||
Ma bibliothèque
|
Ma bibliothèque
|
||||||
</p>
|
</p>
|
||||||
<span class="ml-4">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5"
|
|
||||||
stroke="currentColor" aria-hidden="true" class="w-4 h-4 mx-auto transition-transform rotate-180">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
<app-sidbar />
|
<div class="flex flex-row">
|
||||||
<router-outlet />
|
<app-sidbar></app-sidbar>
|
||||||
|
<div class="flex-1">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<section class="text-gray-600 body-font">
|
||||||
|
<div class="container px-5 py-24 mx-auto">
|
||||||
|
<div class="flex flex-wrap -m-4">
|
||||||
|
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||||
|
<a class="block relative h-48 rounded overflow-hidden">
|
||||||
|
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/420x260">
|
||||||
|
</a>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||||
|
<h2 class="text-gray-900 title-font text-lg font-medium">The Catalyzer</h2>
|
||||||
|
<p class="mt-1">$16.00</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||||
|
<a class="block relative h-48 rounded overflow-hidden">
|
||||||
|
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/421x261">
|
||||||
|
</a>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||||
|
<h2 class="text-gray-900 title-font text-lg font-medium">Shooting Stars</h2>
|
||||||
|
<p class="mt-1">$21.15</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||||
|
<a class="block relative h-48 rounded overflow-hidden">
|
||||||
|
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/422x262">
|
||||||
|
</a>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||||
|
<h2 class="text-gray-900 title-font text-lg font-medium">Neptune</h2>
|
||||||
|
<p class="mt-1">$12.00</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||||
|
<a class="block relative h-48 rounded overflow-hidden">
|
||||||
|
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/423x263">
|
||||||
|
</a>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||||
|
<h2 class="text-gray-900 title-font text-lg font-medium">The 400 Blows</h2>
|
||||||
|
<p class="mt-1">$18.40</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||||
|
<a class="block relative h-48 rounded overflow-hidden">
|
||||||
|
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/424x264">
|
||||||
|
</a>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||||
|
<h2 class="text-gray-900 title-font text-lg font-medium">The Catalyzer</h2>
|
||||||
|
<p class="mt-1">$16.00</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||||
|
<a class="block relative h-48 rounded overflow-hidden">
|
||||||
|
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/425x265">
|
||||||
|
</a>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||||
|
<h2 class="text-gray-900 title-font text-lg font-medium">Shooting Stars</h2>
|
||||||
|
<p class="mt-1">$21.15</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||||
|
<a class="block relative h-48 rounded overflow-hidden">
|
||||||
|
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/427x267">
|
||||||
|
</a>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||||
|
<h2 class="text-gray-900 title-font text-lg font-medium">Neptune</h2>
|
||||||
|
<p class="mt-1">$12.00</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||||
|
<a class="block relative h-48 rounded overflow-hidden">
|
||||||
|
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/428x268">
|
||||||
|
</a>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||||
|
<h2 class="text-gray-900 title-font text-lg font-medium">The 400 Blows</h2>
|
||||||
|
<p class="mt-1">$18.40</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|||||||
0
src/app/public/pages/search/search.component.css
Normal file
0
src/app/public/pages/search/search.component.css
Normal file
0
src/app/public/pages/search/search.component.html
Normal file
0
src/app/public/pages/search/search.component.html
Normal file
19
src/app/public/pages/search/search.component.ts
Normal file
19
src/app/public/pages/search/search.component.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {ReactiveFormsModule} from '@angular/forms';
|
||||||
|
import {NgForOf, NgIf} from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-search',
|
||||||
|
imports: [
|
||||||
|
NgIf,
|
||||||
|
NgForOf,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './search.component.html',
|
||||||
|
styleUrl: './search.component.css'
|
||||||
|
})
|
||||||
|
export class SearchComponent {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user