diff --git a/.env b/.env new file mode 100644 index 00000000..b6272163 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +JWT_SECRET="yffghfhgdgfdrtvkbhj" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4d6352ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/videos/* +.env \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..adcd8c62 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/zikos.iml b/.idea/zikos.iml new file mode 100644 index 00000000..cf2da3e7 --- /dev/null +++ b/.idea/zikos.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..399939f5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,65 @@ +ARG GO_VERSION=1.22.5 +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build +WORKDIR /src + + +COPY . . + +RUN go mod download -x + +ARG TARGETARCH + + +# Build the Go application +RUN CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /bin/server . + + +FROM alpine:latest AS final + + +# Install runtime dependencies +RUN apk --update add \ + ca-certificates \ + tzdata \ + && \ + update-ca-certificates + + +# Create non-privileged user +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser + + + + + +RUN apk add --no-cache gcc g++ git openssh-client +RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server +# Set the working directory to /app +WORKDIR /app + +# Copy the executable from the build stage +COPY --from=build /bin/server /bin/ + +# Copy the frontend directory from the build stage +COPY --from=build /src/videos /app/videos + +# Ensure the appuser has ownership of the frontend directory +RUN chown -R appuser:appuser /app/videos + +# Set the user to the non-privileged user +USER appuser + +# Expose the port the app runs on +EXPOSE 4000 + +# Start the application +ENTRYPOINT [ "/bin/server" ] + diff --git a/README.md b/README.md deleted file mode 100644 index 2bdeb028..00000000 --- a/README.md +++ /dev/null @@ -1,54 +0,0 @@ -
- - ![GitHub repo size](https://img.shields.io/github/repo-size/codewithsadee/grilli) - ![GitHub stars](https://img.shields.io/github/stars/codewithsadee/grilli?style=social) - ![GitHub forks](https://img.shields.io/github/forks/codewithsadee/grilli?style=social) -[![Twitter Follow](https://img.shields.io/twitter/follow/codewithsadee_?style=social)](https://twitter.com/intent/follow?screen_name=codewithsadee_) - [![YouTube Video Views](https://img.shields.io/youtube/views/CjVGp5kGHxA?style=social)](https://youtu.be/CjVGp5kGHxA) - -
-
- -

Grilli - Restaurant Website

- - Grilli is a fully responsive restaurant website,
Responsive for all devices, build using HTML, CSS, and JavaScript. - - ➥ Live Demo - -
- -
- -### Demo Screeshots - -![Grilli Desktop Demo](./readme-images/desktop.png "Desktop Demo") - -### Prerequisites - -Before you begin, ensure you have met the following requirements: - -* [Git](https://git-scm.com/downloads "Download Git") must be installed on your operating system. - -### Run Locally - -To run **Grilli** locally, run this command on your git bash: - -Linux and macOS: - -```bash -sudo git clone https://github.com/codewithsadee/grilli.git -``` - -Windows: - -```bash -git clone https://github.com/codewithsadee/grilli.git -``` - -### Contact - -If you want to contact with me you can reach me at [Twitter](https://www.twitter.com/codewithsadee). - -### License - -[MIT](https://choosealicense.com/licenses/mit/) diff --git a/assets/css/style.css b/assets/css/style.css index 14743c6e..bfebf622 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -325,7 +325,7 @@ body.nav-active { overflow: hidden; } .section-subtitle::after { content: url('../images/separator.svg'); display: block; - width: 100px; + width: 100px; margin-inline: auto; margin-block-start: 5px; } @@ -344,6 +344,12 @@ body.nav-active { overflow: hidden; } z-index: 1; } +.text-gold-crayola{ + + background-color: var(--gold-crayola); + color: var(--gold-crayola); +} + .btn::before { content: ""; position: absolute; diff --git a/assets/images/Thumbs.db b/assets/images/Thumbs.db deleted file mode 100644 index d7afd570..00000000 Binary files a/assets/images/Thumbs.db and /dev/null differ diff --git a/assets/images/WhatsApp Image 2024-08-29 at 5.40.58 PM (1).jpeg b/assets/images/WhatsApp Image 2024-08-29 at 5.40.58 PM (1).jpeg new file mode 100644 index 00000000..cb2e1885 Binary files /dev/null and b/assets/images/WhatsApp Image 2024-08-29 at 5.40.58 PM (1).jpeg differ diff --git a/assets/images/chefs.jpg b/assets/images/chefs.jpg new file mode 100644 index 00000000..439abf2b Binary files /dev/null and b/assets/images/chefs.jpg differ diff --git a/assets/images/foodtaste.jpg b/assets/images/foodtaste.jpg new file mode 100644 index 00000000..cdf2e9f3 Binary files /dev/null and b/assets/images/foodtaste.jpg differ diff --git a/assets/images/serving.jpg b/assets/images/serving.jpg new file mode 100644 index 00000000..51b50206 Binary files /dev/null and b/assets/images/serving.jpg differ diff --git a/assets/images/witty.png b/assets/images/witty.png new file mode 100644 index 00000000..8e99cabc Binary files /dev/null and b/assets/images/witty.png differ diff --git a/assets/js/script.js b/assets/js/script.js index 1f2f6f87..ae6faa20 100644 --- a/assets/js/script.js +++ b/assets/js/script.js @@ -166,5 +166,6 @@ window.addEventListener("mousemove", function (event) { y = y * Number(parallaxItems[i].dataset.parallaxSpeed); parallaxItems[i].style.transform = `translate3d(${x}px, ${y}px, 0px)`; } + -}); \ No newline at end of file +}); diff --git a/backend/apis/admin_apis.go b/backend/apis/admin_apis.go new file mode 100644 index 00000000..95e8e22a --- /dev/null +++ b/backend/apis/admin_apis.go @@ -0,0 +1,20 @@ +package apis + +import ( + "database/sql" + "net/http" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" +) + +func AdminApis(r *gin.Engine, db *sql.DB) error { + + var err error + + http.HandleFunc("/admin/login", func(w http.ResponseWriter, r *http.Request) { + + http.ServeFile(w, r, "login.html") + }) + return err +} diff --git a/backend/apis/apis.go b/backend/apis/apis.go new file mode 100644 index 00000000..de527359 --- /dev/null +++ b/backend/apis/apis.go @@ -0,0 +1,803 @@ +package apis + +import ( + "database/sql" + "log" + "runtime" + "strconv" + "zikos/backend/auth" + "zikos/backend/models" + + "fmt" + + "net/http" + "os" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" +) + +var db *sql.DB + +func StartServer(dbportNumber int) error { + + var err error + host := "localhost" + port := dbportNumber + user := "postgres" + password := "1590" + dbname := "zikos" + + conn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname) + db, err = sql.Open("postgres", conn) + if err != nil { + panic(err) + } + + err = db.Ping() + if err != nil { + panic(err) + } + fmt.Println("Connected To DB") + + r := gin.Default() + + r.Static("/static", "./frontend/static") + r.Static("/thumbnails", "./thumbnails") + + r.LoadHTMLFiles("./frontend/pages/admin/dashboard/picture_manager.html", "./frontend/pages/admin/dashboard/video_manager.html", "./frontend/pages/client/Landing.html", + "./frontend/pages/admin/dashboard/login.html", "./frontend/pages/admin/dashboard/event_manager.html", + "./frontend/pages/admin/dashboard/dashboard_index.html", "./frontend/pages/admin/dashboard/profile_manager.html", + ) + + http.HandleFunc("/admin/login", func(w http.ResponseWriter, r *http.Request) { + + http.ServeFile(w, r, "login.html") + }) + + r.GET("/login", func(c *gin.Context) { + + c.HTML(http.StatusOK, "login.html", nil) + }) + r.GET("/", func(c *gin.Context) { + + c.HTML(http.StatusOK, "Landing.html", nil) + }) + + r.GET("/getVideos", getVideos) + + r.GET("/video/:id", StreamVideoHandler) + + r.DELETE("/video/:id", deleteVideo) + + r.GET("/video_manager", auth.AuthMiddleware(), func(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "video_manager.html", nil) + }) + r.GET("/picture_manager", auth.AuthMiddleware(), func(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "picture_manager.html", nil) + }) + + r.GET("/profile_manager", auth.AuthMiddleware(), func(ctx *gin.Context) { + + ctx.HTML(http.StatusOK, "profile_manager.html", nil) + }) + + r.GET("/dashboard_index", auth.AuthMiddleware(), func(ctx *gin.Context) { + + ctx.HTML(http.StatusOK, "dashboard_index.html", nil) + }) + + r.GET("/event_manager", auth.AuthMiddleware(), func(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "event_manager.html", nil) + }) + + r.POST("/register", auth.Register(db)) + r.POST("/login", auth.Login(db), func(ctx *gin.Context) { + + }) + + r.GET("/events", getEvents) + r.PATCH("/events", updateEvent) + r.POST("/events", createEvent) + r.DELETE("/events/:id", deleteEvent) + r.POST("/userDetails/:id", getUserDetails) + r.POST("/updateProfile", updateProfile) + + r.POST("/updatePassword", updatePassword) + r.GET("/pictures", getPictures) + + r.POST("/pictures", addPicture) + r.DELETE("/pictures/:id", deletePicture) + r.GET("/getLandingPageVideos", getLandingPageVideos) + + r.POST("/uploadVideo", uploadVideo) + + r.POST("/logout", logoutUser) + return http.ListenAndServe(":8080", r) + +} + +func getVideos(c *gin.Context) { + // Execute the query + rows, err := db.Query(`SELECT * FROM landing_videos`) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var videos []models.Video + + // Loop through rows + for rows.Next() { + var video models.Video + + // Scan each row + err := rows.Scan(&video.Id, &video.Name, &video.Link, &video.IsOnDash, &video.Thumbnail) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + videos = append(videos, video) + } + + // Check for errors encountered during row iteration + if err = rows.Err(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Send the response if no errors occurred + c.JSON(http.StatusOK, gin.H{ + "videos": videos, + }) +} + +func uploadVideo(c *gin.Context) { + + imageTitle := c.PostForm("videoName") + formData, err := c.FormFile("video") + + if err != nil { + fmt.Println(err) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "message": "No file is received", + }) + return + } + thumbnanil, err := c.FormFile("thumbnail") + + // The file cannot be received. + if err != nil { + fmt.Println(err) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "message": "No file is received", + }) + return + } + + directory, err := os.Getwd() + if err != nil { + + fmt.Println(err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + } + + // file, err := formData.Open() + os := runtime.GOOS + + var video_directory string + var image_directory string + var db_path string + + switch os { + case "windows": + + video_directory = fmt.Sprintf("%s\\videos\\%s", directory, formData.Filename) + image_directory = fmt.Sprintf("%s\\thumbnails\\%s", directory, thumbnanil.Filename) + db_path = fmt.Sprintf("\\thumbnails\\%s", thumbnanil.Filename) + case "linux": + video_directory = fmt.Sprintf("%s/videos/%s", directory, formData.Filename) + image_directory = fmt.Sprintf("%s/thumbnails/%s", directory, thumbnanil.Filename) + db_path = fmt.Sprintf("/thumbnails/%s", thumbnanil.Filename) + + } + + fmt.Println("video_directory", video_directory) + fmt.Println("image_directory", image_directory) + fmt.Print("os", os) + + if err := c.SaveUploadedFile(formData, video_directory); err != nil { + fmt.Println(err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + "message": "Unable to save the video", + }) + return + } + if err := c.SaveUploadedFile(thumbnanil, image_directory); err != nil { + fmt.Println(err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + + "message": "Unable to save the image", + }) + return + } + + _, err2 := db.Exec("INSERT INTO landing_videos (name, url_link,thumbnail, is_on_dash) VALUES ($1, $2, $3, $4)", imageTitle, video_directory, db_path, true) + + if err2 != nil { + + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + "message": err2.Error(), + }) + } + // File saved successfully. Return proper result + c.JSON(http.StatusOK, gin.H{ + "message": "Your file has been successfully uploaded.", + }) + +} + +func getLandingPageVideos(c *gin.Context) { + + rows, err := db.Query(`SELECT * FROM landing_videos`) + + if err != nil { + + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var videos []models.Video + + // Loop through rows + for rows.Next() { + var video models.Video + + // Scan each row + err := rows.Scan(&video.Id, &video.Name, &video.Link, &video.IsOnDash) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + videos = append(videos, video) + } + + //Create a video stream for each videofile using its link + + c.JSON(http.StatusOK, gin.H{ + "videos": videos, + }) +} + +// Streaming handler for videos +func StreamVideoHandler(c *gin.Context) { + // Parse the video ID from the URL + videoID, err := strconv.Atoi(c.Param("id")) + fmt.Println(videoID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid video ID"}) + return + } + + // Fetch video metadata from the database + video, err := getVideoByID(videoID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Video not found"}) + return + } + + // Serve the video file using the path from the database + c.File(video.Link) +} + +func getVideoByID(id int) (*models.Video, error) { + var video models.Video + row := db.QueryRow(`SELECT id, name, url_link FROM landing_videos WHERE id = $1`, id) + if err := row.Scan(&video.Id, &video.Name, &video.Link); err != nil { + return nil, err + } + fmt.Println(video.Link) + return &video, nil +} + +func getEvents(c *gin.Context) { + + rows, err := db.Query("SELECT * FROM events") + + if err != nil { + + c.JSON(http.StatusInternalServerError, gin.H{ + "failed to query database": err.Error(), + }) + } + + defer rows.Close() + + var events []models.EventResponse + for rows.Next() { + + var event models.EventResponse + + if err := rows.Scan(&event.Id, &event.EventName, &event.EventDescription, &event.DateOfEvent); err != nil { + + c.JSON(500, gin.H{ + "Failed to Scan Database Rows": err.Error(), + }) + } + + events = append(events, event) + + } + c.JSON(200, gin.H{ + "events": events, + }) + +} + +func deleteEvent(c *gin.Context) { + + event_id, err := strconv.Atoi(c.Param("id")) + + if err != nil { + log.Fatal("Failed to Parse Paramerter, or parameter is empty") + c.JSON(500, gin.H{ + "failed to parse parameter": err.Error(), + }) + + } + + _, err = db.Exec("DELETE FROM events where id = $1", event_id) + + if err != nil { + log.Fatal("Failed to Delete Event, or parameter is empty") + c.JSON(500, gin.H{ + "failed to Delete Event": err.Error(), + }) + + } + fmt.Println("event successfully deleted") + + c.JSON(200, gin.H{ + "message": "event successfully deleted", + }) + +} + +func updateEvent(c *gin.Context) { + + var event models.EventResponse + c.BindJSON(&event) + + _, err := db.Exec(`UPDATE TABLE events + SET event_name = $1,event_description = $2, date-of_event = $3 + WHERE id = $4 + `, &event.EventName, &event.EventDescription, &event.DateOfEvent, &event.Id) + + if err != nil { + log.Fatal("Failed to Update Event, or parameter is empty") + c.JSON(500, gin.H{ + "failed to Update Event": err.Error(), + }) + + } + + c.JSON(200, gin.H{ + "message": "event successfully Updated", + }) + +} + +func createEvent(c *gin.Context) { + var event models.EventRequest + + // Bind JSON input to the event struct + if err := c.BindJSON(&event); err != nil { + fmt.Println(err) + c.JSON(400, gin.H{ + "error": "Invalid JSON input", + }) + return + } + + // Validate the fields + if event.EventName == "" || event.EventDescription == "" || event.DateOfEvent == "" { + + c.JSON(400, gin.H{ + "error": "All fields (event_name, event_description, date_of_event) are required", + }) + return + } + + // Insert into the database + _, err := db.Exec( + `INSERT INTO events (event_name, event_description, date_of_event) VALUES ($1, $2, $3)`, + event.EventName, event.EventDescription, event.DateOfEvent, + ) + + // Handle database errors + if err != nil { + c.JSON(500, gin.H{ + "error": "Failed to add event", + "details": err.Error(), + }) + return + } + + // Success response + c.JSON(200, gin.H{ + "message": "Event successfully added", + }) +} + +func deleteVideo(c *gin.Context) { + + id, err := strconv.Atoi(c.Param("id")) + link := c.Query("link") + + fmt.Println(link) + if err != nil { + + c.JSON(500, gin.H{ + "failed to parse parameter": err.Error(), + }) + } + + err = os.Remove(link) + if err != nil { + + c.JSON(500, gin.H{ + "failed to Delete Video": err.Error(), + }) + } + _, err = db.Exec("DELETE FROM landing_videos where id = $1", id) + + if err != nil { + + c.JSON(500, gin.H{ + "failed to delete Video from db": err.Error(), + }) + } + + c.JSON(200, gin.H{ + "message": "Video successfully deleted", + }) + +} + +func getPictures(c *gin.Context) { + + rows, err := db.Query(`SELECT * FROM pictures`) + + if err != nil { + fmt.Println(err) + c.JSON(500, gin.H{ + "error": err.Error(), + }) + } + defer rows.Close() + + var pictures []models.Photo + for rows.Next() { + var picture models.Photo + + if err = rows.Scan(&picture.Id, &picture.Url, &picture.IsOnHome, &picture.PictureName); err != nil { + + c.JSON(500, gin.H{ + "error": err.Error(), + }) + } + + pictures = append(pictures, picture) + + } + + c.JSON(200, gin.H{ + "pictures": pictures, + }) + +} + +func addPicture(c *gin.Context) { + + picture, err := c.FormFile("image") + + if err != nil { + c.JSON(400, gin.H{ + "error": err.Error(), + }) + } + directory, err := os.Getwd() + + os := runtime.GOOS + if err != nil { + c.JSON(500, gin.H{ + "error": err.Error(), + }) + } + picture_name := picture.Filename + + var db_path string + var image_directory string + + switch os { + case "windows": + + image_directory = fmt.Sprintf(`%s\frontend\static\images\%s`, directory, picture_name) + db_path = fmt.Sprintf(`\static\images\%s`, picture_name) + case "linux": + + image_directory = fmt.Sprintf("%s/frontend/static/images/%s", directory, picture_name) + db_path = fmt.Sprintf("/static/images/%s", picture_name) + + } + + err = c.SaveUploadedFile(picture, image_directory) + + if err != nil { + c.JSON(500, gin.H{ + "error": err.Error(), + }) + } + + _, err = db.Exec("INSERT INTO pictures (url_link, is_on_dash, picture_name) VALUES ($1, $2, $3)", db_path, false, picture_name) + + if err != nil { + + c.JSON(500, gin.H{ + "error": err.Error(), + }) + } + + c.JSON(200, gin.H{ + "message": "Picture successfully added", + }) +} + +func deletePicture(c *gin.Context) { + // Parse the ID from the URL parameter + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(400, gin.H{"error": "Invalid picture ID"}) + return + } + + // Query the database for the picture name + var pictureName string + row := db.QueryRow("SELECT picture_name FROM pictures WHERE id = $1", id) + if err := row.Scan(&pictureName); err != nil { + c.JSON(404, gin.H{"error": "Picture not found"}) + return + } + + // Determine the file path based on OS + directory, err := os.Getwd() + if err != nil { + c.JSON(500, gin.H{"error": "Failed to determine working directory"}) + return + } + + var imageDirectory string + switch runtime.GOOS { + case "windows": + imageDirectory = fmt.Sprintf("%s\\frontend\\static\\images\\%s", directory, pictureName) + case "linux": + imageDirectory = fmt.Sprintf("%s/frontend/static/images/%s", directory, pictureName) + default: + c.JSON(500, gin.H{"error": "Unsupported operating system"}) + return + } + + // Delete the file + if err := os.Remove(imageDirectory); err != nil && !os.IsNotExist(err) { + c.JSON(500, gin.H{"error": "Failed to delete picture file"}) + return + } + + // Delete the database entry + _, err = db.Exec("DELETE FROM pictures WHERE id = $1", id) + if err != nil { + c.JSON(500, gin.H{"error": "Failed to delete picture record from database"}) + return + } + + // Success response + c.JSON(200, gin.H{"message": "Picture successfully deleted"}) +} + +func logoutUser(c *gin.Context) { + + // Clear specific cookies + c.SetCookie("Authorization", "", -1, "/", "", false, true) + + c.JSON(200, gin.H{ + "message": "User successfully logged out", + }) +} + +func getUserDetails(c *gin.Context) { + + var userId int + var err error + userId, err = strconv.Atoi(c.Param("id")) + + if err != nil { + c.JSON(400, gin.H{ + "error": "invalid user id", + }) + return + } + + var user models.User + rows := db.QueryRow(`SELECT id, first_name, last_name, email, role FROM users WHERE id = $1`, userId) + + if err = rows.Scan(&user.Id, &user.FirstName, &user.LastName, &user.Email, &user.Role); err != nil { + c.JSON(500, gin.H{ + "error": err.Error(), + }) + + } + switch { + case err == sql.ErrNoRows: + c.JSON(404, gin.H{ + "error": "user not found", + }) + return + + case err != nil: + c.JSON(500, gin.H{ + "error": err.Error(), + }) + return + + } + user.Password = "" + + c.JSON(200, gin.H{ + "user": user, + }) +} + +func updateProfile(c *gin.Context) { + + var user models.User + + // Bind JSON input to the user struct + if err := c.BindJSON(&user); err != nil { + c.JSON(400, gin.H{ + "error": "Invalid JSON input", + }) + return + } + + // Update user details in the database + _, err := db.Exec( + `UPDATE users SET first_name = $1, last_name = $2, email = $3 WHERE id = $4`, + user.FirstName, user.LastName, user.Email, user.Id) + if err != nil { + c.JSON(500, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(200, gin.H{ + "message": "User details successfully updated", + }) + +} + +func updatePassword(c *gin.Context) { + // Request struct with validation tags + type UpdatePasswordRequest struct { + Id int `json:"id" binding:"required,min=1"` + OldPassword string `json:"old_password" binding:"required,min=8,max=72"` + NewPassword string `json:"new_password" binding:"required,min=8,max=72"` + } + + var updatePasswordRequest UpdatePasswordRequest + + // Validate JSON input with detailed error handling + if err := c.ShouldBindJSON(&updatePasswordRequest); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid input", + "details": err.Error(), + }) + return + } + + // Prevent password reuse + if updatePasswordRequest.OldPassword == updatePasswordRequest.NewPassword { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "New password must be different from the old password", + }) + return + } + + // Retrieve current password from database + var currentHashedPassword string + err := db.QueryRow( + `SELECT password FROM users WHERE id = $1`, + updatePasswordRequest.Id, + ).Scan(¤tHashedPassword) + + if err != nil { + if err == sql.ErrNoRows { + c.JSON(http.StatusNotFound, gin.H{ + "error": "User not found", + }) + } else { + log.Printf("Database error retrieving user: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal server error", + }) + } + return + } + + // Verify current password + if err := auth.VerifyPassword(updatePasswordRequest.OldPassword, currentHashedPassword); err != nil { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "Invalid current password", + }) + return + } + + // Hash new password before storing + hashedNewPassword, err := auth.HashPassword(updatePasswordRequest.NewPassword) + if err != nil { + log.Printf("Password hashing error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Unable to process new password", + }) + return + } + + // Update password in database with transaction + tx, err := db.Begin() + if err != nil { + log.Printf("Transaction start error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal server error", + }) + return + } + defer tx.Rollback() // Rollback in case of any error + + _, err = tx.Exec( + `UPDATE users SET + password = $1, + last_password_change = CURRENT_TIMESTAMP + WHERE id = $2`, + hashedNewPassword, + updatePasswordRequest.Id, + ) + if err != nil { + log.Printf("Password update error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to update password", + }) + return + } + + // Commit transaction + if err := tx.Commit(); err != nil { + log.Printf("Transaction commit error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to complete password update", + }) + return + } + + // Log password change event (optional but recommended) + go func() { + log.Printf("Password changed for user ID: %d", updatePasswordRequest.Id) + }() + + c.JSON(http.StatusOK, gin.H{ + "message": "Password successfully updated", + }) +} diff --git a/backend/apis/client_apis.go b/backend/apis/client_apis.go new file mode 100644 index 00000000..1f92b466 --- /dev/null +++ b/backend/apis/client_apis.go @@ -0,0 +1,21 @@ +package apis + +import ( + "database/sql" + "net/http" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" +) + +func ClientApis(r *gin.Engine, db *sql.DB) error { + + var err error + + http.HandleFunc("/videos", func(w http.ResponseWriter, r *http.Request) { + + http.ServeFile(w, r, "video_1.mp4") + }) + + return err +} diff --git a/backend/auth/auth.go b/backend/auth/auth.go new file mode 100644 index 00000000..d4e1648c --- /dev/null +++ b/backend/auth/auth.go @@ -0,0 +1,305 @@ +package auth + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + "regexp" + "strings" + "time" + + "zikos/backend/models" + + "github.com/dgrijalva/jwt-go" + "github.com/gin-gonic/gin" + "golang.org/x/crypto/bcrypt" +) + +const ( + // Bcrypt cost factor + bcryptCost = 14 + + // Token expiration time + tokenExpiration = 24 * time.Hour +) + +var ( + // Email validation regex + emailRegex = regexp.MustCompile(`^[A-Za-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) +) + +// GetSecretKey retrieves the JWT secret key from environment variable +func GetSecretKey() []byte { + secretKey := os.Getenv("JWT_SECRET") + if secretKey == "" { + // Fallback to a default secret (only for development, never in production!) + secretKey = "development_secret_key_please_set_env_variable" + fmt.Println("WARNING: Using default secret key. Set JWT_SECRET environment variable!") + } + return []byte(secretKey) +} + +// AuthMiddleware provides JWT authentication for routes +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + + tokenString, err := c.Cookie("Authorization") + + if err != nil { + + fmt.Println(err.Error()) + + } + + if tokenString == "" { + // Redirect unauthenticated users to login with the current path as the redirect target + target := c.Request.URL.Path + c.Redirect(http.StatusFound, fmt.Sprintf("/login?redirect=%s", target)) + c.Abort() + return + } + + // Remove "Bearer " prefix if present + if len(tokenString) > 7 && tokenString[:7] == "Bearer " { + tokenString = tokenString[7:] + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return GetSecretKey(), nil + }) + + if err != nil || !token.Valid { + // Redirect to login if the token is invalid + target := c.Request.URL.Path + c.Redirect(http.StatusFound, fmt.Sprintf("/login?redirect=%s", target)) + c.Abort() + return + } + + c.Next() + } +} + +// isValidEmail checks if the email matches the required format +func isValidEmail(email string) bool { + return emailRegex.MatchString(email) +} + +// VerifyPassword compares a plain text password with a hashed password +func VerifyPassword(password, hashedPassword string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} + +// HashPassword generates a bcrypt hash of the password +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// Login handles user authentication +func Login(db *sql.DB) gin.HandlerFunc { + return func(c *gin.Context) { + var user models.User + if err := c.ShouldBindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid input", + }) + return + } + + // Validate email format + if !isValidEmail(user.Email) { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid email format", + }) + return + } + + var hashedPassword, userID string + var role int + err := db.QueryRow( + `SELECT id, first_name, last_name, password, role FROM users WHERE email = $1`, + user.Email, + ).Scan(&userID, &user.FirstName, &user.LastName, &hashedPassword, &role) + + if err == sql.ErrNoRows { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "Invalid credentials", + }) + return + } else if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Database error", + }) + return + } + + // Verify password + if err := VerifyPassword(user.Password, hashedPassword); err != nil { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "Invalid credentials", + }) + return + } + + // Generate JWT + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "userID": userID, + "email": user.Email, + "role": role, + "exp": time.Now().Add(tokenExpiration).Unix(), + }) + + tokenString, err := token.SignedString(GetSecretKey()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate token", + }) + return + } + + c.SetCookie("Authorization", "Bearer "+tokenString, int(tokenExpiration.Seconds()), "/", "", false, true) + + redirect := c.DefaultQuery("redirect", "/dashboard_index") + c.JSON(http.StatusOK, gin.H{ + "token": tokenString, + "userID": userID, + "role": role, + "username": user.FirstName + " " + user.LastName, + "message": "Login successful", + "redirect": redirect, + }) + + } +} + +// Register handles user registration +func Register(db *sql.DB) gin.HandlerFunc { + return func(c *gin.Context) { + var user models.User + if err := c.ShouldBindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid input", + "details": err.Error(), + }) + return + } + + // Trim and normalize email + user.Email = strings.TrimSpace(strings.ToLower(user.Email)) + + // Comprehensive email validation + emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`) + if !emailRegex.MatchString(user.Email) { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid email format", + }) + return + } + + // Validate password strength + if len(user.Password) < 8 { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Password must be at least 8 characters long", + }) + return + } + + // Check if user already exists + var exists bool + err := db.QueryRow( + "SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)", + user.Email, + ).Scan(&exists) + if err != nil { + log.Printf("Database exists check error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Database error", + }) + return + } + if exists { + c.JSON(http.StatusConflict, gin.H{ + "error": "User with this email already exists", + }) + return + } + + // Hash password + hashedPassword, err := HashPassword(user.Password) + if err != nil { + log.Printf("Password hashing error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to secure password", + }) + return + } + + // Default role (adjust as needed) + defaultRole := 2 // Assuming 1 is admin, 2 is regular user + + // Start a transaction + tx, err := db.Begin() + if err != nil { + log.Printf("Transaction start error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal server error", + }) + return + } + defer tx.Rollback() // Rollback in case of any error + + // Insert new user and retrieve ID + var userID int + err = tx.QueryRow( + `INSERT INTO users + (first_name, last_name, email, password, role) + VALUES + ($1, $2, $3, $4, $5) + RETURNING id`, + user.FirstName, + user.LastName, + user.Email, + hashedPassword, + defaultRole, + ).Scan(&userID) + + if err != nil { + log.Printf("User insertion error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create user", + }) + return + } + + // Commit the transaction + if err := tx.Commit(); err != nil { + log.Printf("Transaction commit error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to complete registration", + }) + return + } + + // Log user registration (optional) + go func() { + log.Printf("New user registered: ID %d, Email %s", userID, user.Email) + }() + + // Respond with user details (excluding sensitive information) + c.JSON(http.StatusCreated, gin.H{ + "message": "User created successfully", + "userID": userID, + "firstName": user.FirstName, + "email": user.Email, + }) + } +} diff --git a/backend/config.go b/backend/config.go new file mode 100644 index 00000000..fb80d572 --- /dev/null +++ b/backend/config.go @@ -0,0 +1,28 @@ +package config + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +// LoadEnvironmentVariables loads environment variables from .env file +func LoadEnvironmentVariables() { + // Check if we're in production + if os.Getenv("APP_ENV") != "production" { + // Load .env file + if err := godotenv.Load(); err != nil { + log.Println("No .env file found") + } + } +} + +// GetEnvOrDefault returns the environment variable value or a default +func GetEnvOrDefault(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} diff --git a/backend/db/zikos.sql b/backend/db/zikos.sql new file mode 100644 index 00000000..49ce60b3 --- /dev/null +++ b/backend/db/zikos.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS landing_videos ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + url_link VARCHAR(255) NOT NULL, + is_on_dash BOOLEAN NOT NULL +); + +CREATE TABLE IF NOT EXISTS events ( + id SERIAL PRIMARY KEY, + event_name VARCHAR(50) NOT NULL, + event_description VARCHAR(255) NOT NULL, + date_of_event DATE NOT NULL +); + +CREATE TABLE IF NOT EXISTS pictures ( + id SERIAL PRIMARY KEY, + url_link VARCHAR(255) NOT NULL, -- Increased to 255 for longer URLs + is_on_dash BOOLEAN, + picture_name VARCHAR(100) NOT NULL +); + +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL, + password VARCHAR(100) NOT NULL, + role INT NOT NULL -- Removed trailing comma +); + diff --git a/backend/models/admin.go b/backend/models/admin.go new file mode 100644 index 00000000..ea62c378 --- /dev/null +++ b/backend/models/admin.go @@ -0,0 +1,7 @@ +package models + +type Admin struct { + Email string `json:"email"` + Username string `json:"username"` + Password string `json:"password"` +} diff --git a/backend/models/event.go b/backend/models/event.go new file mode 100644 index 00000000..22983567 --- /dev/null +++ b/backend/models/event.go @@ -0,0 +1,14 @@ +package models + +type EventResponse struct { + Id int `json:"id"` + EventName string `json:"event_name"` + EventDescription string `json:"event_description"` + DateOfEvent string `json:"date_of_event"` +} + +type EventRequest struct { + EventName string `json:"event_name"` + EventDescription string `json:"event_description"` + DateOfEvent string `json:"date_of_event"` +} diff --git a/backend/models/photo.go b/backend/models/photo.go new file mode 100644 index 00000000..64eef9ea --- /dev/null +++ b/backend/models/photo.go @@ -0,0 +1,8 @@ +package models + +type Photo struct { + Id int `json:"id"` + Url string `json:"url_link"` + IsOnHome bool `json:"is_on_dash"` + PictureName string `json:"picture_name"` +} diff --git a/backend/models/user.go b/backend/models/user.go new file mode 100644 index 00000000..7d9ea4cc --- /dev/null +++ b/backend/models/user.go @@ -0,0 +1,10 @@ +package models + +type User struct { + Id int `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + Password string `json:"password"` + Role int `json:"role"` +} diff --git a/backend/models/video.go b/backend/models/video.go new file mode 100644 index 00000000..516b16bb --- /dev/null +++ b/backend/models/video.go @@ -0,0 +1,9 @@ +package models + +type Video struct { + Id int `json:"id"` + Name string `json:"name"` + Link string `json:"url_link"` + Thumbnail string `json:"thumbnail"` + IsOnDash bool `json:"is_on_dash"` +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..bcf32941 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3.8' + +services: + postgres: + image: postgres:latest + container_name: postgres_db + environment: + POSTGRES_PASSWORD: "1590" + ports: + - "5500:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + restart: always + +volumes: + postgres_data: diff --git a/frontend/index.js b/frontend/index.js new file mode 100644 index 00000000..f71f539c --- /dev/null +++ b/frontend/index.js @@ -0,0 +1,20 @@ +document.addEventListener('DOMContentLoaded', async () => { + + video_container = document.getElementById('video') + const myVideos = await getVideos(); + console.log(myVideos[0].Link) + + video_container.InnerHTML = ` + + + ` + + +}) +async function getVideos() { + const response = await fetch('/videos') + const videos = await response.json() + return videos.videos +} \ No newline at end of file diff --git a/frontend/pages/admin/dashboard/dashboard_index.html b/frontend/pages/admin/dashboard/dashboard_index.html new file mode 100644 index 00000000..00d269fc --- /dev/null +++ b/frontend/pages/admin/dashboard/dashboard_index.html @@ -0,0 +1,58 @@ + + + + + + Dashboard + + + + + + +
+ + + + +
+ +
+ +

Welcome Back!

+
+ + +
+ +
+

Total Pictures

+

1,235

+ View All +
+
+

Total Videos

+

542

+ View All +
+
+

Upcoming Events

+

15

+ View All +
+ + +
+

Recent Pictures

+
+
+
+
+
+
+
+
+
+
+ + diff --git a/frontend/pages/admin/dashboard/event_manager.html b/frontend/pages/admin/dashboard/event_manager.html new file mode 100644 index 00000000..3c290cb9 --- /dev/null +++ b/frontend/pages/admin/dashboard/event_manager.html @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + Event Manager + + + + + + + + + + + + + +
+ + + + + + + +
+ +
+
+ + + + + +
+

Event Manager

+
+

Total Events -

+ +
+
+
+ + + + + + + + + + + + + +
A list of your available Events.
Event TitleEvent DescriptionEvent DateActions
+
+
+
+ + + + + diff --git a/frontend/pages/admin/dashboard/events_manager.html b/frontend/pages/admin/dashboard/events_manager.html new file mode 100644 index 00000000..e69de29b diff --git a/frontend/pages/admin/dashboard/getPics.php b/frontend/pages/admin/dashboard/getPics.php new file mode 100644 index 00000000..1e912d70 --- /dev/null +++ b/frontend/pages/admin/dashboard/getPics.php @@ -0,0 +1,111 @@ + + + + + + Document + + + + +$error

"; + +}else{ + + + + +} + +?> + + + + + + +getMessage(); + + + } + + + + + return [$data, $error]; +} + + + + +if($error){ + echo $error; +}else{ + echo json_encode($data); +} + + + + +?> \ No newline at end of file diff --git a/frontend/pages/admin/dashboard/index.html b/frontend/pages/admin/dashboard/index.html new file mode 100644 index 00000000..1da8f414 --- /dev/null +++ b/frontend/pages/admin/dashboard/index.html @@ -0,0 +1,37 @@ + + + + + + + + + Document + + + +
+

Edit Video Details

+ + + + + + + + + + +
+ + + +
+
+ + + \ No newline at end of file diff --git a/frontend/pages/admin/dashboard/index.php b/frontend/pages/admin/dashboard/index.php new file mode 100644 index 00000000..6313e88b --- /dev/null +++ b/frontend/pages/admin/dashboard/index.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/frontend/pages/admin/dashboard/login.html b/frontend/pages/admin/dashboard/login.html new file mode 100644 index 00000000..c4ae79b9 --- /dev/null +++ b/frontend/pages/admin/dashboard/login.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + Login + + + +
+

Login

+
+
+ + +
+
+ + +
+
+ + + Forgot Password? + +
+
+
+ + + \ No newline at end of file diff --git a/frontend/pages/admin/dashboard/picture_manager.html b/frontend/pages/admin/dashboard/picture_manager.html new file mode 100644 index 00000000..e8e46dff --- /dev/null +++ b/frontend/pages/admin/dashboard/picture_manager.html @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + Picture Manager + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ +
+
+ + +
+ +

Picture Manager

+
+

Total Pictures -

+ +
+
+ + +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/pages/admin/dashboard/profile_manager.html b/frontend/pages/admin/dashboard/profile_manager.html new file mode 100644 index 00000000..c8fc7654 --- /dev/null +++ b/frontend/pages/admin/dashboard/profile_manager.html @@ -0,0 +1,132 @@ + + + + + + + + + + + Profile Manager + + + + +
+ + + +
+

Account Manager

+

Manage Your Account Settings and Preferences

+
+
+ +
+ + + +
+
+

Personal Info

+

Edit your personal information below

+
+
+ +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+
+ + +
+ +
+ +
+ +
+
+

Security Info

+

Edit your security information below

+
+
+ +
+ +
+ + + +
+ +
+ + + + +
+ +
+ + + + + +
+
+
+
+
+ + +
+ +
+ +
+ + +
+ + + + + \ No newline at end of file diff --git a/frontend/pages/admin/dashboard/unauthorised.html b/frontend/pages/admin/dashboard/unauthorised.html new file mode 100644 index 00000000..0cb819a8 --- /dev/null +++ b/frontend/pages/admin/dashboard/unauthorised.html @@ -0,0 +1,51 @@ + + + + + + Not Authorized + + + + + + +
+

Session Expired. Please Login Again

+ Login +
+ + + \ No newline at end of file diff --git a/frontend/pages/admin/dashboard/video_manager.html b/frontend/pages/admin/dashboard/video_manager.html new file mode 100644 index 00000000..9ddc10b2 --- /dev/null +++ b/frontend/pages/admin/dashboard/video_manager.html @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + Video Manager + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ +
+
+ + + + + +
+

Video Manager

+
+

Total Videos -

+ +
+
+
+ + + + + + + + + + + + + +
A list of your available Videos.
TitleDurationStatusActions
+
+
+
+ + + + + + + + diff --git a/frontend/pages/client/Landing.html b/frontend/pages/client/Landing.html new file mode 100644 index 00000000..cc3e2f62 --- /dev/null +++ b/frontend/pages/client/Landing.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + Zikos Restaurant - Experience Fine Dining in Lusaka + + + + + + + + + +
+ + +
+
+
+
+ + Delicious Food + + +
+ + +
+

+ Welcome to Witty Foods +

+

+ Experience the finest dining with our chef-crafted menu, featuring + fresh and locally sourced ingredients. +

+ +
+
+ + + + + + + +
+
+ +
+
+ +
+
+ + +
+ +
+
+

Upcoming Events

+
+
+
+ + + +
+

Our Signature Dishes

+ +
+ + +
+ +
+ +
+
+

Get in Touch

+ +
+ +
+

Contact Details

+

Reach out to us for reservations, inquiries, or feedback!

+ +
+

📍 Location: 123 Food Street, Lusaka, Zambia

+

📞 Phone: +260 900 000 000

+

📧 Email: info@foodwebsite.com

+

🕒 Hours: Mon-Sat: 9 AM - 10 PM, Sun: 10 AM - 8 PM

+
+ + + +
+ + +
+

Send Us a Message

+
+ + + + +
+
+
+ + +
+ +
+
+
+ + + + + + + + + + + +
+
+ + + + + + + \ No newline at end of file diff --git a/index.html b/frontend/pages/client/inderx.html similarity index 78% rename from index.html rename to frontend/pages/client/inderx.html index 7cabfd9b..99670abd 100644 --- a/index.html +++ b/frontend/pages/client/inderx.html @@ -9,7 +9,7 @@ - Grilli - Amazing & Delicious Food + Wiity Foods- By The Zikos @@ -28,14 +28,14 @@ - + - - - + + + @@ -47,7 +47,7 @@
-

Grilli

+

WITTY FOODS

@@ -67,7 +67,7 @@ - Restaurant St, Delicious City, London 9578, UK + Ibex Hill, Lusaka, Zambia @@ -86,17 +86,18 @@ - +1 123 456 7890 + +260954047633/0979816832 + /0972962474
- +
- booking@restaurant.com + cookingwiththezikos@gmail.com
@@ -114,7 +115,7 @@
- + - + @@ -323,6 +332,9 @@

+ @@ -332,11 +344,12 @@

Flavors For Royalty

-

We Offer Top Notch

+

Every Flavor Tells a Story

- Lorem Ipsum is simply dummy text of the printing and typesetting industry lorem Ipsum has been the industrys - standard dummy text ever. + Our content is food-centric. We understand that food holds a special place in + various cultures, serving as a means to preserve traditions and connect people + across the globe.

- shape - shape @@ -424,6 +437,11 @@

+ + + + + @@ -435,17 +453,33 @@

Our Story

-

Every Fla vor Tells a Story

+

We Offer Top Notch

- Lorem Ipsum is simply dummy text of the printingand typesetting industry lorem Ipsum has been the - industrys standard dummy text ever since the when an unknown printer took a galley of type and scrambled - it to make a type specimen book It has survived not only five centuries, but also the leap into. + Cooking with the Zikos is a family business, proudly owned by Zambians, + established in 2021. + With a humble start, we are rapidly expanding to become one of the largest + family-owned cooking entities in the country, focusing on food, travel, and + brand influencing. + The brand is exclusively owned by Mr. Syanabandi Pikinini Ziko and Mrs. Chola + Kaunda Ziko, as their names imply. A Zambian couple deeply committed to + food, family, love, and ethical values.

+
Book Through Call
- +80 (400) 123 4567 + + 0954047633 Read More @@ -457,21 +491,21 @@

Every Fla vor Tells a Story

- about banner
-
- +
- + @@ -487,22 +521,21 @@

Every Fla vor Tells a Story

- special dish
- badge + badge

Special Dish

Lobster Tortellini

- Lorem Ipsum is simply dummy text of the printingand typesetting industry lorem Ipsum has been the - industrys standard dummy text ever since the when an unknown printer took a galley of type. + Lobster tortellini is a delightful dish that combines the rich, succulent flavor of lobster with the delicate texture of tortellini pasta

@@ -520,9 +553,9 @@

Lobster Tortellini

- + - +
@@ -547,7 +580,7 @@

Delicious Menu