build improvements

This commit is contained in:
Schneider Roland 2025-02-23 14:57:49 +01:00
parent 4d239f7c7a
commit 5c3fe39074
50 changed files with 1672 additions and 212 deletions

View File

@ -26,6 +26,11 @@ services:
ports: ports:
- "4012:5432" - "4012:5432"
yogamail:
image: dockage/mailcatcher:0.9.0
ports:
- "4013:1080"
- "4014:1025"
volumes: volumes:
next-db: {} next-db: {}

View File

@ -1 +1 @@
202501081712.9d7bb39 202502042213.4d239f7

View File

@ -9,4 +9,4 @@ echo "build image ${TAG} from folder ${PROJECT_ROOT}"
cd $PROJECT_ROOT cd $PROJECT_ROOT
docker build --platform linux/amd64 -t $TAG . docker build -t $TAG .

View File

@ -1,4 +0,0 @@
#!/usr/bin/env bash
ENV_DEV_DIR=$(readlink -f "${CURRENT_DIR}/../../../environments/dev/docker-compose")
COMPOSE_FILE=$(readlink -f "/docker-compose.yml")

View File

@ -1,8 +0,0 @@
#!/usr/bin/env bash
CURRENT_DIR=$(dirname "$0")
source "${CURRENT_DIR}/start.docker.compose.env.sh"
cd "${ENV_DEV_DIR}"
echo "starting compose file in ${ENV_DEV_DIR}"
docker compose up -d

View File

@ -1,18 +0,0 @@
#!/usr/bin/env bash
CURRENT_DIR=$(dirname "$0")
source "${CURRENT_DIR}/build.docker.env.sh"
source "${CURRENT_DIR}/start.docker.compose.env.sh"
echo "updating image version to ${VERSION} in compose file ${COMPOSE_FILE}"
#export TAG=docker.rschneider.hu/infra/yogastic:$VERSION
case $(uname) in
"Darwin")
echo "Detected macOS"
sed -i'' -e "s/docker.rschneider.hu\\/infra\\/yogastic:.*/docker.rschneider.hu\\/infra\\/yogastic:$VERSION/g" $COMPOSE_FILE
;;
*)
sed -i "s/docker.rschneider.hu\\/infra\\/yogastic:.*/docker.rschneider.hu\\/infra\\/yogastic:$VERSION/g" $COMPOSE_FILE
#sed -i "s/docker.rschneider.hu\\/infra\\/yogastic:.*/docker.rschneider.hu\\/infra\\/yogastic:$VERSION/g" $COMPOSE_FILE
;;
esac

View File

@ -1,10 +0,0 @@
#!/usr/bin/env bash
CURRENT_DIR=$(dirname "$0")
source "${CURRENT_DIR}/build.docker.env.sh"
source "${CURRENT_DIR}/start.docker.compose.env.sh"
cd ${ENV_DEV_DIR}
echo "Destroy compose in ${ENV_DEV_DIR}"
docker compose down -v

View File

@ -1,10 +0,0 @@
#!/usr/bin/env bash
CURRENT_DIR=$(dirname "$0")
source "${CURRENT_DIR}/build.docker.env.sh"
source "${CURRENT_DIR}/start.docker.compose.env.sh"
cd ${ENV_DEV_DIR}
echo "Stopping compose in ${ENV_DEV_DIR}"
docker compose down

View File

@ -1 +1 @@
202501071722.a8b144f 202502042213.4d239f7

View File

@ -0,0 +1,49 @@
pipeline {
agent any
environment {
DOCKER_IMAGE = 'yoga-cms'
DOCKER_REGISTRY = 'your-docker-registry'
DOCKER_CREDENTIALS_ID = 'your-docker-credentials-id'
}
stages {
stage('Checkout') {
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/main']], userRemoteConfigs: [[url: 'https://gitea.rschneider.hu/rschneider/yogastic.git', credentialsId: 'rschneider_gitea.rschneider.hu']]])
}
}
stage('Build Docker Image') {
steps {
script {
// docker.build("${DOCKER_IMAGE}:${env.BUILD_ID}")
}
}
}
stage('Push Docker Image') {
steps {
script {
// docker.withRegistry("https://${DOCKER_REGISTRY}", DOCKER_CREDENTIALS_ID) {
// docker.image("${DOCKER_IMAGE}:${env.BUILD_ID}").push()
// }
}
}
}
stage('Cleanup') {
steps {
script {
docker.image("${DOCKER_IMAGE}:${env.BUILD_ID}").remove()
}
}
}
}
post {
always {
cleanWs()
}
}
}

View File

@ -21,4 +21,5 @@ echo $CMD1
echo $CMD2 echo $CMD2
${CMD1} ${CMD1}
${CMD2} ${CMD2}
echo "done" echo "done"

View File

@ -10,7 +10,28 @@ const compat = new FlatCompat({
}); });
const eslintConfig = [ const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"), ...compat.config(
{
extends:[
"next/core-web-vitals",
"next/typescript",
],
// rules: {
// "@typescript-eslint/no-explicit-any": "off"
// },
}
),
// {
// name: "custom",
// "files": ["src/types/generated-strapi-interfaces/api/article.ts"], // Or *.test.js
// rules: {
// "@typescript-eslint/no-explicit-any": "off"
// },
// }
]; ];
console.info("eslint config",eslintConfig)
export default eslintConfig; export default eslintConfig;

View File

@ -3,6 +3,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
output: "standalone", output: "standalone",
sassOptions: { sassOptions: {
silenceDeprecations: [ silenceDeprecations: [
'abs-percent', 'abs-percent',

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@strapi/database": "^5.10.3",
"@types/aos": "^3.0.7", "@types/aos": "^3.0.7",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
@ -18,6 +19,9 @@
"@types/ityped": "^1.0.3", "@types/ityped": "^1.0.3",
"@types/pg": "^8.11.10", "@types/pg": "^8.11.10",
"@types/qs": "^6.9.18", "@types/qs": "^6.9.18",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"ajv-i18n": "^4.2.0",
"aos": "^2.3.4", "aos": "^2.3.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bootstrap": "^4.6.2", "bootstrap": "^4.6.2",
@ -25,16 +29,19 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"ityped": "^1.0.3", "ityped": "^1.0.3",
"next": "15.1.3", "next": "15.1.3",
"nodemailer": "^6.10.0",
"pg": "^8.13.1", "pg": "^8.13.1",
"qs": "^6.14.0", "qs": "^6.14.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-bootstrap": "^2.10.9", "react-bootstrap": "^2.10.9",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"validator": "^13.12.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^6.4.17",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",

View File

@ -0,0 +1,65 @@
'use server'
import {sendMail} from "@/actions/mail-actions";
import {contactUsFormValidator} from "@/validation/validation";
import {copyFormValues, copyValidationErrors, FormControl, formDataToJson} from "@/util/form-util";
import localize_hu from 'ajv-i18n/localize/hu';
export type ContactFormState = {
firstname: FormControl,
lastname: FormControl,
phone: FormControl,
email: FormControl,
comment: FormControl,
formState: 'empty' | 'invalid' | 'success'
}
export type ContactFormData = {
firstname: string,
lastname: string,
phone: string,
email: string,
comment: string,
}
export async function sendContactUsMail(prevState: ContactFormState, formState: FormData) {
const newState: ContactFormState = {
comment: {name: 'comment'}, email: {name: 'email'}, firstname: {name: 'firstname'},
lastname: {name: 'lastname'},
phone: {name: 'phone'},
formState: 'invalid'
};
const contactUsFormData: ContactFormData = formDataToJson(formState) as ContactFormData;
const validationResult = contactUsFormValidator(contactUsFormData);
localize_hu(contactUsFormValidator.errors)
if (!validationResult) {
copyFormValues(newState, contactUsFormData);
copyValidationErrors(newState, contactUsFormValidator)
} else {
sendMail(
{
email: contactUsFormData.email,
sendTo: process.env.SITE_MAIL_RECIEVER,
subject: 'Kapcsolat',
text: `
Kapcsolat üzenet érkezett:
Név: ${contactUsFormData.lastname} ${contactUsFormData.firstname}
Tel: ${contactUsFormData.phone}
Email: ${contactUsFormData.email}
Üzenet:
${contactUsFormData.comment}
`,
html:`
Kapcsolat üzenet érkezett:
<br>Név: ${contactUsFormData.lastname} ${contactUsFormData.firstname}
<br>Tel: ${contactUsFormData.phone}
<br>Email: ${contactUsFormData.email}
<br>Üzenet:
<br>${contactUsFormData.comment}
`,
}
)
newState.formState = 'success';
}
return newState;
}

View File

@ -0,0 +1,50 @@
'use server';
import nodemailer from 'nodemailer';
const SMTP_SERVER_HOST = process.env.SMTP_SERVER_HOST;
const SMTP_SERVER_PORT =parseInt( process.env.SMTP_SERVER_PORT!,10);
const SMTP_SERVER_SECURE = "true" == process.env.SMTP_SERVER_SECIRE ;
const SMTP_SERVER_USERNAME = process.env.SMTP_SERVER_USERNAME;
const SMTP_SERVER_PASSWORD = process.env.SMTP_SERVER_PASSWORD;
const SITE_MAIL_RECIEVER = process.env.SITE_MAIL_RECIEVER;
const transporter = nodemailer.createTransport({
// service: 'gmail',
host: SMTP_SERVER_HOST,
port: SMTP_SERVER_PORT,
secure: SMTP_SERVER_SECURE,
auth: {
user: SMTP_SERVER_USERNAME,
pass: SMTP_SERVER_PASSWORD,
},
});
export async function sendMail({
email,
sendTo,
subject,
text,
html,
}: {
email: string;
sendTo?: string;
subject: string;
text: string;
html?: string;
}) {
try {
const isVerified = await transporter.verify();
console.log("isVerified",isVerified)
} catch (error) {
console.error('Something Went Wrong', SMTP_SERVER_USERNAME, SMTP_SERVER_PASSWORD, error);
return;
}
const info = await transporter.sendMail({
from: email,
to: sendTo || SITE_MAIL_RECIEVER,
subject: subject,
text: text,
html: html ? html : '',
});
console.log('Message Sent', info.messageId);
console.log('Mail sent to',sendTo, SITE_MAIL_RECIEVER);
return info;
}

View File

@ -1,17 +0,0 @@
export interface HomePageData{
}
export interface OurServiceData{
}
export interface AboutUsData{
}
export interface OurSpecialitiesData{
}

View File

@ -1,10 +1,5 @@
import React from "react"; import React from "react";
import AosComponent from "@/components/aos.component"; import AosComponent from "@/components/aos.component";
import OurServicesComponent from "@/components/our.services.component";
import OurSpecialitiesComponent from "@/components/our.specialities.component";
import ContactUsComponent from "@/components/contact.us.component";
import PricingComponent from "@/components/pricing.component";
import FeedbackComponent from "@/components/feedbackComponent";
import BlogPostsComponent from "@/components/blog.posts.component"; import BlogPostsComponent from "@/components/blog.posts.component";
import FooterComponent from "@/components/footer.component"; import FooterComponent from "@/components/footer.component";
import SubscribeComponent from "@/components/subscribe.component"; import SubscribeComponent from "@/components/subscribe.component";
@ -43,11 +38,6 @@ export default async function About() {
{ ourVision && <TextWithImageComponent config={ourVision} />} { ourVision && <TextWithImageComponent config={ourVision} />}
{ achievements && <AchievementsComponent config={achievements}/>} { achievements && <AchievementsComponent config={achievements}/>}
{/*{ ourServices && <OurServicesComponent title={ourServices?.title!} header={ourServices?.header!} description={ourServices?.description!} /> }*/}
{/*{ ourSpecialities && <OurSpecialitiesComponent config={ourSpecialities} /> }*/}
{/*{ contactUs && <ContactUsComponent contactUs={contactUs}/>}*/}
{/*{ prices && <PricingComponent config={prices} /> }*/}
{/*{ feedbacks && <FeedbackComponent config={feedbacks} /> }*/}
{ blogs && <BlogPostsComponent config={blogs} />} { blogs && <BlogPostsComponent config={blogs} />}
{ subscribeNow && <SubscribeComponent config={subscribeNow} /> } { subscribeNow && <SubscribeComponent config={subscribeNow} /> }
{ footer && <FooterComponent config={footer} />} { footer && <FooterComponent config={footer} />}
@ -59,6 +49,5 @@ export default async function About() {
); );
} }
export const getBreadCrumbName = () => {
return "Rólunk;" export const dynamic = 'force-dynamic'
}

View File

@ -4,3 +4,4 @@ export default function BlogListComponent(){
<div>Blog list</div> <div>Blog list</div>
) )
} }
export const dynamic = 'force-dynamic'

View File

@ -1,7 +1,5 @@
import strapiApi from "@/api/strapi/strapi-api"; import strapiApi from "@/api/strapi/strapi-api";
import SubHeaderComponent from "@/components/subHeader.component"; import SubHeaderComponent from "@/components/subHeader.component";
import FaqComponent from "@/components/faq.component";
import BlogPostsComponent from "@/components/blog.posts.component";
import SubscribeComponent from "@/components/subscribe.component"; import SubscribeComponent from "@/components/subscribe.component";
import FooterComponent from "@/components/footer.component"; import FooterComponent from "@/components/footer.component";
import AosComponent from "@/components/aos.component"; import AosComponent from "@/components/aos.component";
@ -34,3 +32,5 @@ export default async function ContactPage(){
); );
} }
export const dynamic = 'force-dynamic'

View File

@ -1,11 +1,5 @@
import React from "react"; import React from "react";
import AosComponent from "@/components/aos.component"; import AosComponent from "@/components/aos.component";
import OurServicesComponent from "@/components/our.services.component";
import AboutUsComponent from "@/components/about.us.component";
import OurSpecialitiesComponent from "@/components/our.specialities.component";
import ContactUsComponent from "@/components/contact.us.component";
import PricingComponent from "@/components/pricing.component";
import FeedbackComponent from "@/components/feedbackComponent";
import BlogPostsComponent from "@/components/blog.posts.component"; import BlogPostsComponent from "@/components/blog.posts.component";
import FooterComponent from "@/components/footer.component"; import FooterComponent from "@/components/footer.component";
import SubscribeComponent from "@/components/subscribe.component"; import SubscribeComponent from "@/components/subscribe.component";
@ -24,7 +18,7 @@ export default async function About() {
footer footer
} = await strapiApi.getFaqPage(); } = await strapiApi.getFaqPage();
return ( return (
<> <>xxxxx
{ <SubHeaderComponent header1={header} description={description} /> } { <SubHeaderComponent header1={header} description={description} /> }
{ questionsAndAnswers && <FaqComponent config={questionsAndAnswers} /> } { questionsAndAnswers && <FaqComponent config={questionsAndAnswers} /> }
{ blogs && <BlogPostsComponent config={blogs} /> } { blogs && <BlogPostsComponent config={blogs} /> }
@ -36,3 +30,4 @@ export default async function About() {
); );
} }
export const dynamic = 'force-dynamic'

View File

@ -13,3 +13,4 @@ const Login: React.FC = () => {
export default Login; export default Login;

View File

@ -50,3 +50,5 @@ export default async function Home() {
); );
} }
export const dynamic = 'force-dynamic'

View File

@ -38,3 +38,5 @@ export default async function PricesPage( ) {
); );
} }
export const dynamic = 'force-dynamic'

View File

@ -1,10 +1,8 @@
import React from "react"; import React from "react";
import AosComponent from "@/components/aos.component"; import AosComponent from "@/components/aos.component";
import OurServicesComponent from "@/components/our.services.component"; import OurServicesComponent from "@/components/our.services.component";
import AboutUsComponent from "@/components/about.us.component";
import OurSpecialitiesComponent from "@/components/our.specialities.component"; import OurSpecialitiesComponent from "@/components/our.specialities.component";
import ContactUsComponent from "@/components/contact.us.component"; import ContactUsComponent from "@/components/contact.us.component";
import PricingComponent from "@/components/pricing.component";
import FeedbackComponent from "@/components/feedbackComponent"; import FeedbackComponent from "@/components/feedbackComponent";
import BlogPostsComponent from "@/components/blog.posts.component"; import BlogPostsComponent from "@/components/blog.posts.component";
import FooterComponent from "@/components/footer.component"; import FooterComponent from "@/components/footer.component";
@ -39,3 +37,5 @@ export default async function Services() {
); );
} }
export const dynamic = 'force-dynamic'

View File

@ -1,6 +1,5 @@
import YogaImageComponent from "@/components/yoga.image.component"; import YogaImageComponent from "@/components/yoga.image.component";
import { import {
YogaAboutUsComponent,
YogaAboutUsComponent_Plain YogaAboutUsComponent_Plain
} from "@/types/generated-strapi-interfaces/api/yoga-about-us-component"; } from "@/types/generated-strapi-interfaces/api/yoga-about-us-component";
import {StrapiFile} from "@/types/types"; import {StrapiFile} from "@/types/types";

View File

@ -7,7 +7,7 @@ export interface Props{
config: YogaAboutUsWithBoxesComponent_Plain config: YogaAboutUsWithBoxesComponent_Plain
} }
export default function AboutUsWithBoxesComponent({ config: {title,header,description,image, box1,box2,box3,box4}}: Props){ export default function AboutUsWithBoxesComponent({ config: {title,header,description, box1,box2,box3,box4}}: Props){
return ( return (

View File

@ -8,7 +8,7 @@ export interface Props{
} }
export default function AchievementsItemComponent({achievement export default function AchievementsItemComponent({achievement
:{ image,title} :{ image}
}: Props){ }: Props){
const imageFile: StrapiFile = image as StrapiFile; const imageFile: StrapiFile = image as StrapiFile;

View File

@ -1,5 +1,4 @@
import YogaImageComponent from "@/components/yoga.image.component"; import YogaImageComponent from "@/components/yoga.image.component";
import {YogaBlogPostsComponent_Plain} from "@/types/generated-strapi-interfaces/api/yoga-blog-posts-component";
import {YogaBlogPost_Plain} from "@/types/generated-strapi-interfaces/api/yoga-blog-post"; import {YogaBlogPost_Plain} from "@/types/generated-strapi-interfaces/api/yoga-blog-post";
import {StrapiFile} from "@/types/types"; import {StrapiFile} from "@/types/types";
import strapiApi from "@/api/strapi/strapi-api"; import strapiApi from "@/api/strapi/strapi-api";

View File

@ -4,7 +4,7 @@ import { useEffect } from "react";
export default function BootstrapComponent() export default function BootstrapComponent()
{ {
useEffect(()=>{ useEffect(()=>{
// @ts-ignore // @ts-expect-error promise
import( "bootstrap/dist/js/bootstrap.bundle") import( "bootstrap/dist/js/bootstrap.bundle")
},[]) },[])
return <></> return <></>

View File

@ -1,12 +1,9 @@
'use client' 'use client'
import React, { ReactNode } from 'react' import React from 'react'
import {usePathname, useRouter} from 'next/navigation' import {usePathname} from 'next/navigation'
import Link from 'next/link'
type Props = {
}
const pathToBreadCrumbs = (path: string) => { const pathToBreadCrumbs = (path: string) => {
const mapping: Record<string, string> = { const mapping: Record<string, string> = {
@ -34,13 +31,13 @@ const NextBreadcrumb = () => {
<div className="btn_wrapper"> <div className="btn_wrapper">
<span className="sub_home_span">{pathToBreadCrumbs( "/" )} </span> <span className="sub_home_span">{pathToBreadCrumbs( "/" )} </span>
{ {
pathNames.map((value,index) => { pathNames.map((value) => {
return ( return (
<> <React.Fragment key={value}>
<i className="fa-solid fa-angles-right" aria-hidden="true"></i> <i className="fa-solid fa-angles-right" aria-hidden="true"></i>
<span className="sub_span">{pathToBreadCrumbs( value )}</span> <span className="sub_span">{pathToBreadCrumbs( value )}</span>
</> </React.Fragment>
) )
}) })
} }

View File

@ -1,10 +1,43 @@
'use client'
import YogaImageComponent from "@/components/yoga.image.component"; import YogaImageComponent from "@/components/yoga.image.component";
import {YogaContactUs_Plain} from "@/types/generated-strapi-interfaces/api/yoga-contact-us"; import {YogaContactUs_Plain} from "@/types/generated-strapi-interfaces/api/yoga-contact-us";
import {RefObject, startTransition, useActionState, useEffect, useRef} from "react";
import {ContactFormState, sendContactUsMail} from "@/actions/contactus-actions";
import clsx from "clsx";
export interface Props{ export interface Props {
contactUs: YogaContactUs_Plain contactUs: YogaContactUs_Plain
} }
const ContactUsComponent = ( { contactUs :{ header,firstName,lastName,phone,title,message,email,buttonText }}: Props) => {
const ContactUsComponent = ({
contactUs: {
header,
firstName,
lastName,
phone,
title,
message,
email,
buttonText
}
}: Props) => {
const initialState = () => {
return {
firstname: {value: ''},
lastname: {value: ''},
comment: {value: ''},
email: {value: ''},
phone: {value: ''}
} as ContactFormState;
}
const [state, formAction] = useActionState(sendContactUsMail, initialState());
const ref: RefObject<HTMLFormElement|null> = useRef(null)
useEffect(() => {
if ( state.formState == 'success'){
ref.current?.reset()
}
}, [state]);
return ( return (
<section className="get_in_touch_section"> <section className="get_in_touch_section">
<div className="container"> <div className="container">
@ -13,39 +46,67 @@ const ContactUsComponent = ( { contactUs :{ header,firstName,lastName,phone,ti
<div className="get_in_touch_content"> <div className="get_in_touch_content">
<h5>{title}</h5> <h5>{title}</h5>
<h2>{header}</h2> <h2>{header}</h2>
<form> <form
ref={ref}
onSubmit={(e) => {
e.preventDefault();
startTransition(() => formAction(new FormData(e.currentTarget)));
}}
>
<div className="row"> <div className="row">
<div className="col-lg-6 col-md-6 col-sm-6"> <div className="col-lg-6 col-md-6 col-sm-6">
<div className="form-group mb-0"> <div className="form-group mb-0">
<input type="text" name="fname" id="fname" className="form-control" <input type="text" name="firstname" id="firstname"
placeholder={firstName}/> className={clsx("form-control", {"mb-0": state.firstname.error})}
placeholder={firstName}
/>
</div> </div>
{state.firstname.error &&
<div className="text-danger mb-3">{state.firstname.error}</div>}
</div> </div>
<div className="col-lg-6 col-md-6 col-sm-6"> <div className="col-lg-6 col-md-6 col-sm-6">
<div className="form-group mb-0"> <div className="form-group mb-0">
<input type="text" name="lname" id="lname" <input type="text" name="lastname" id="lname"
className="form-control form_style" placeholder={lastName}/> className={clsx("form-control", "form_style", {"mb-0": state.lastname.error})}
placeholder={lastName}/>
</div> </div>
{state.lastname.error &&
<div className="text-danger mb-3">{state.lastname.error}</div>}
</div> </div>
<div className="col-lg-6 col-md-6 col-sm-6"> <div className="col-lg-6 col-md-6 col-sm-6">
<div className="form-group mb-0"> <div className="form-group mb-0">
<input type="tel" name="phonenum" id="phonenum" className="form-control" <input type="tel" name="phone" id="phonenum"
className={clsx("form-control", {"mb-0": state.phone.error})}
placeholder={phone}/> placeholder={phone}/>
</div> </div>
{state.phone.error &&
<div className="text-danger mb-3">{state.phone.error}</div>}
</div> </div>
<div className="col-lg-6 col-md-6 col-sm-6"> <div className="col-lg-6 col-md-6 col-sm-6">
<div className="form-group mb-0"> <div className="form-group mb-0">
<input type="email" name="emailaddrs" id="emailaddrs" <input type="email" name="email" id="emailaddrs"
className="form-control form_style" placeholder={email}/> className={clsx("form-control", "form_style", {"mb-0": state.email.error})}
placeholder={email}/>
</div> </div>
{state.email.error &&
<div className="text-danger mb-3">{state.email.error}</div>}
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="col-lg-12"> <div className="col-lg-12">
<div className=" form-group mb-0"> <div className=" form-group mb-0">
<textarea rows={3} name="comment" id="msg" className="form-control" <textarea rows={3} name="comment" id="msg"
className={clsx("form-control", {"mb-0": state.comment.error})}
placeholder={message}></textarea> placeholder={message}></textarea>
</div> </div>
{state.comment.error &&
<div className="text-danger mb-3">{state.comment.error}</div>}
</div> </div>
</div> </div>
<div className="btn_wrapper"> <div className="btn_wrapper">

View File

@ -2,7 +2,6 @@
import {YogaFaqComponent_Plain} from "@/types/generated-strapi-interfaces/api/yoga-faq-component"; import {YogaFaqComponent_Plain} from "@/types/generated-strapi-interfaces/api/yoga-faq-component";
import {useState} from "react"; import {useState} from "react";
import FaqQaComponent from "@/components/faq.qa.component"; import FaqQaComponent from "@/components/faq.qa.component";
import { MouseEvent } from 'react'
export interface Props { export interface Props {
@ -53,6 +52,7 @@ export default function FaqComponent({
questionsAndAnswers.map((qa, index) => { questionsAndAnswers.map((qa, index) => {
return ( return (
<FaqQaComponent <FaqQaComponent
key={qa.id}
qa={qa} qa={qa}
id={index} id={index}
onClick={onQAClick} onClick={onQAClick}

View File

@ -2,27 +2,18 @@ import YogaImageComponent from "@/components/yoga.image.component";
import {YogaFaqQa_Plain} from "@/types/generated-strapi-interfaces/api/yoga-faq-qa"; import {YogaFaqQa_Plain} from "@/types/generated-strapi-interfaces/api/yoga-faq-qa";
import clsx from "clsx"; import clsx from "clsx";
import classes from "./faq.qa.module.scss";
import {useEffect, useState} from "react";
export interface Props{ export interface Props{
id: number, id: number,
qa: YogaFaqQa_Plain, qa: YogaFaqQa_Plain,
isOpen: boolean, isOpen: boolean,
onClick: (id: number) => void onClick: (id: number) => void
} }
export default function FaqQaComponent({id,qa,isOpen,onClick}: Props){ export default function FaqQaComponent({id,qa,isOpen}: Props){
return ( return (
<div className="accordion-card"> <div className="accordion-card">
<div className="card-header" id={"heading" + id}> <div className="card-header" id={"heading" + id}>
<a href="#" <a href="#"
// onClick={ (event) => {
// event.preventDefault();
// onClick(id)
// }}
className={clsx("btn", "btn-link", {"collapsed": !isOpen})} className={clsx("btn", "btn-link", {"collapsed": !isOpen})}
data-toggle="collapse" data-target={"#collapse" + id} data-toggle="collapse" data-target={"#collapse" + id}
aria-expanded="false" aria-controls={"collapse" + id}> aria-expanded="false" aria-controls={"collapse" + id}>

View File

@ -1,5 +1,4 @@
import { import {
YogaGoogleMapsComponent,
YogaGoogleMapsComponent_Plain YogaGoogleMapsComponent_Plain
} from "@/types/generated-strapi-interfaces/api/yoga-google-maps-component"; } from "@/types/generated-strapi-interfaces/api/yoga-google-maps-component";

View File

@ -20,7 +20,7 @@ const ITypedComponent = ( {text}: Props ) => {
loop: true loop: true
}) })
setReady(true) setReady(true)
}, [ready]); }, [ready,text]);
return (<></> ); return (<></> );
} }

View File

@ -6,10 +6,8 @@ export interface Props{
const MainHeaderComponent = ({ config: { const MainHeaderComponent = ({ config: {
title, title,
header, header,
headerIType,
description, description,
button, button,
image
}}: Props) => { }}: Props) => {

View File

@ -2,7 +2,6 @@
import {FC} from "react"; import {FC} from "react";
import YogaImageComponent from "@/components/yoga.image.component"; import YogaImageComponent from "@/components/yoga.image.component";
import clsx from "clsx"; import clsx from "clsx";
import exp from "node:constants";
export interface MenuItem{ export interface MenuItem{
href?: string; href?: string;

View File

@ -1,7 +1,4 @@
import YogaImageComponent from "@/components/yoga.image.component"; import YogaImageComponent from "@/components/yoga.image.component";
import {
OurSpecialitiesComponent_Plain
} from "@/types/generated-strapi-interfaces/components/yoga-site/OurSpecialitiesComponent";
import OurSpecialitiesItemComponent from "@/components/our.specialities.item.component"; import OurSpecialitiesItemComponent from "@/components/our.specialities.item.component";
import {YogaSpecialitiesComponent_Plain} from "@/types/generated-strapi-interfaces/api/yoga-specialities-component"; import {YogaSpecialitiesComponent_Plain} from "@/types/generated-strapi-interfaces/api/yoga-specialities-component";

View File

@ -1,7 +1,3 @@
import YogaImageComponent from "@/components/yoga.image.component";
import {
OurSpecialitiesComponent_Plain
} from "@/types/generated-strapi-interfaces/components/yoga-site/OurSpecialitiesComponent";
import {TitleDescription_Plain} from "@/types/generated-strapi-interfaces/components/shared/TitleDescription"; import {TitleDescription_Plain} from "@/types/generated-strapi-interfaces/components/shared/TitleDescription";
import clsx from "clsx"; import clsx from "clsx";

View File

@ -1,4 +1,3 @@
import YogaImageComponent from "@/components/yoga.image.component";
import {YogaPriceComponent_Plain} from "@/types/generated-strapi-interfaces/api/yoga-price-component"; import {YogaPriceComponent_Plain} from "@/types/generated-strapi-interfaces/api/yoga-price-component";
import {PriceItemComponent} from "@/components/price.item.component"; import {PriceItemComponent} from "@/components/price.item.component";

View File

@ -5,10 +5,9 @@ import {HeaderB} from "@/types/generated-strapi-interfaces/components/yoga-site/
import NextBreadcrumb from "@/components/breadcrumbs.component"; import NextBreadcrumb from "@/components/breadcrumbs.component";
export interface Props extends HeaderB{ export type Props = HeaderB ;
}
const SubHeaderComponent = ({header1,header2,description}: Props) =>{ const SubHeaderComponent = ({header1,description}: Props) =>{
return ( return (
<div className="sub-banner-section"> <div className="sub-banner-section">
<Nav menuItems={MAIN_MENU} /> <Nav menuItems={MAIN_MENU} />

View File

@ -5,7 +5,7 @@ import {
import clsx from "clsx"; import clsx from "clsx";
export interface Props{ export interface Props{
config: YogaSubscribeNowComponent_Plain, config: YogaSubscribeNowComponent_Plain,
styleClass: string styleClass?: string
} }
const SubscribeComponent = ({ const SubscribeComponent = ({
config: {title,header,placeHolderEmail,buttonSubscribeLabel}, config: {title,header,placeHolderEmail,buttonSubscribeLabel},

View File

@ -11,6 +11,7 @@ export interface Properties {
const YogaImageComponent = ( {src,alt,className,style}: Properties ) => { const YogaImageComponent = ( {src,alt,className,style}: Properties ) => {
return ( return (
/* eslint-disable @next/next/no-img-element */
<img src={src} alt={alt || ""} className={ clsx( className )} style={style} /> <img src={src} alt={alt || ""} className={ clsx( className )} style={style} />
); );
} }

View File

@ -17,7 +17,7 @@ export interface Article {
cover?: { data: Media }; cover?: { data: Media };
author?: { data: Author }; author?: { data: Author };
category?: { data: Category }; category?: { data: Category };
blocks?: any; blocks?: object;
}; };
} }
export interface Article_Plain { export interface Article_Plain {
@ -28,7 +28,7 @@ export interface Article_Plain {
cover?: Media_Plain; cover?: Media_Plain;
author?: Author_Plain; author?: Author_Plain;
category?: Category_Plain; category?: Category_Plain;
blocks?: any; blocks?: object;
} }
export interface Article_NoRelations { export interface Article_NoRelations {
@ -39,7 +39,7 @@ export interface Article_NoRelations {
cover?: number; cover?: number;
author?: number; author?: number;
category?: number; category?: number;
blocks?: any; blocks?: object;
} }
export interface Article_AdminPanelLifeCycle { export interface Article_AdminPanelLifeCycle {
@ -50,5 +50,5 @@ export interface Article_AdminPanelLifeCycle {
cover?: AdminPanelRelationPropertyModification<Media_Plain>; cover?: AdminPanelRelationPropertyModification<Media_Plain>;
author?: AdminPanelRelationPropertyModification<Author_Plain>; author?: AdminPanelRelationPropertyModification<Author_Plain>;
category?: AdminPanelRelationPropertyModification<Category_Plain>; category?: AdminPanelRelationPropertyModification<Category_Plain>;
blocks?: any; blocks?: object;
} }

View File

@ -0,0 +1,53 @@
import {ValidateFunction} from "ajv";
export type FormControl = {
name: string,
value?: string,
error?: string
}
export function formDataToJson(formData: FormData) {
const object: Record<string, string | string[] | File | File[]> = {};
formData.forEach((value, key) => {
if ( key == 'formState'){
return;
}
// Reflect.has in favor of: object.hasOwnProperty(key)
if (!Reflect.has(object, key)) {
object[key] = value;
return;
}
if (!Array.isArray(object[key])) {
object[key] = [];
}
if (value instanceof File) {
(object[key] as File[]).push(value);
} else {
(object[key] as string[]).push(value);
}
});
return object;
}
/* eslint-disable @typescript-eslint/no-explicit-any */
export function copyFormValues(form: Record<string, FormControl|string|any>,jsonFormData: Record<string, FormDataEntryValue>){
const keys = Object.getOwnPropertyNames(form);
for(const key of keys){
if (key == 'formState'){
continue;
}
const formControl: FormControl = form[key];
formControl.value = jsonFormData[key] as string;
}
}
/* eslint-disable @typescript-eslint/no-explicit-any */
export function copyValidationErrors(form: Record<string, FormControl|string|any >,validateFunction: ValidateFunction){
const keys = Object.getOwnPropertyNames(form);
for(const key of keys){
const formControl: FormControl = form[key];
if ( key == 'formState') continue;
formControl.error = validateFunction.errors?.find( err => err.instancePath =="/"+key)?.message;
}
}

View File

@ -0,0 +1,23 @@
import Ajv, {JSONSchemaType} from "ajv"
import addFormats from "ajv-formats"
import {ContactFormData} from "@/actions/contactus-actions";
const ajv = new Ajv({allErrors: true}) // options can be passed, e.g. {allErrors: true}
addFormats(ajv)
const contactUsFormSchema: JSONSchemaType<ContactFormData> = {
type: "object",
properties: {
firstname: {type: "string",minLength: 3,maxLength: 20},
lastname: {type: "string",minLength: 3,maxLength: 20},
phone: {type: "string",minLength: 3,maxLength: 20},
email: {type: "string", format: 'email'},
comment: {type: "string",minLength: 3,maxLength: 500}
},
required: ["firstname","lastname","phone","email","comment"],
additionalProperties: true
}
export const contactUsFormValidator = ajv.compile(contactUsFormSchema)

View File

@ -7,11 +7,11 @@ ENV NODE_ENV=${NODE_ENV}
WORKDIR /opt/ WORKDIR /opt/
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm install -g node-gyp RUN npm install -g node-gyp
RUN npm config set fetch-retry-maxtimeout 600000 -g && npm install --only=production RUN npm config set fetch-retry-maxtimeout 600000 -g && npm install --only=production --debug
ENV PATH=/opt/node_modules/.bin:$PATH ENV PATH=/opt/node_modules/.bin:$PATH
WORKDIR /opt/app WORKDIR /opt/app
COPY . . COPY . .
RUN npm run build RUN npm run build --debug
# Creating final production image # Creating final production image
FROM node:18-alpine FROM node:18-alpine

31
yoga-cms/Dockerfile.prod Normal file
View File

@ -0,0 +1,31 @@
# Creating multi-stage build for production
FROM node:18-alpine as build
RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev git > /dev/null 2>&1
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /opt/
COPY package.json package-lock.json ./
RUN npm install -g node-gyp
RUN npm config set fetch-retry-maxtimeout 600000 -g && npm install --only=production --debug
ENV PATH=/opt/node_modules/.bin:$PATH
WORKDIR /opt/app
COPY . .
RUN pwd && ls -lah
RUN npm run build --debug
# Creating final production image
FROM node:18-alpine
# RUN apk add --no-cache vips-dev
# ARG NODE_ENV=production
# ENV NODE_ENV=${NODE_ENV}
# WORKDIR /opt/
# COPY --from=build /opt/node_modules ./node_modules
# WORKDIR /opt/app
# COPY --from=build /opt/app ./
# ENV PATH=/opt/node_modules/.bin:$PATH
# RUN chown -R node:node /opt/app
USER node
EXPOSE 1337
# CMD ["npm", "run", "start"]