diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..53f1d66 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,56 @@ +name: Publish Docker image to dockerhub and github packages + +on: + push: + branches: ['main'] + +jobs: + push_to_registries: + name: Push Docker image to multiple registries + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + attestations: write + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: | + akashsingh04/haal_samachar + ghcr.io/${{ github.repository }} + + - name: Build and push Docker images + id: push + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + diff --git a/README.md b/README.md index 6072043..04d9f87 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ -## HaalSamachar : A Blog Website built with GoLang+Gin in the backend and NextJs+TypeScript in the frontend with PostgreSQL powered database. +## HaalSamachar : A Blog Website built with GoLang with multiple services which include a graphQL API using gqlgen and three REST APIs built using Gin and NextJs+TypeScript in the frontend with PostgreSQL powered database, containerized using Docker and deployed using Kubernetes. -This GoLang application is designed to serve as a blog website that implements multiple services for separate containerization and scaling. ## Features -- **Blog Website**: The application provides basic functionalities of a blog website, including creating, reading, updating, and deleting blog posts. -- **Multiple Services**: The application is divided into multiple services, each responsible for specific functionalities such as user authentication, post management, and comment handling. -- **Contract Testing**: Utilizes contract testing to verify the interactions and agreements between different services, ensuring that they work together seamlessly. -- **Modular Design**: Built with a modular design approach, making it easy to add new services or modify existing ones without impacting the entire application. +- graphQL server using gqlgen , rest server using gin mongo, docker, kubernetes , nextjs ssr. +- Write about CI/CD +Tech: GO : gin and gqlgen +Docker and kubernetes +Nextjs : Ts and tailwind +Posgresql +firebase auth ## Getting Started @@ -29,7 +31,9 @@ To get started with this application, follow these steps: 5. **Access the Application**: Once the services are running, access the blog website through your web browser at respective localhost ports [ 8081 , 8082 , 8083 , 8084 ]. -## API Documentation +## API Documentation + +refactor this t0o two files: graphQL api docs and rest api docs #### The API is deployed on 4 different services on Render (Thank the lord for their free tier) diff --git a/frontend/app/blogs/[blogid]/page.tsx b/frontend/app/blogs/[blogid]/page.tsx index 3c38b2d..952d4de 100644 --- a/frontend/app/blogs/[blogid]/page.tsx +++ b/frontend/app/blogs/[blogid]/page.tsx @@ -1,107 +1,111 @@ 'use client' import React, { useEffect, useState } from 'react'; import Header from '../../components/Header'; -import { usePathname } from 'next/navigation' -import Comments from '../../components/Comments' -import CommentForm from '../../components/CommentForm' -import Markdown from 'react-markdown' +import { usePathname } from 'next/navigation'; +import Comments from '../../components/Comments'; +import CommentForm from '../../components/CommentForm'; +import Markdown from 'react-markdown'; import Likes from '@/app/components/Likes'; -import DeleteDialogueBox from "../../components/DeleteDialogueBox"; -import { auth } from "../../firebase"; -import { onAuthStateChanged } from "firebase/auth"; +import DeleteDialogueBox from '../../components/DeleteDialogueBox'; +import { auth } from '../../firebase'; +import { onAuthStateChanged } from 'firebase/auth'; +import { useQuery, gql } from '@apollo/client'; import SpotifyCard from '@/app/components/SpotifyCard'; +//REST API URL const usersAPI = process.env.NEXT_PUBLIC_USERS_API_URL; -const blogsAPI = process.env.NEXT_PUBLIC_BLOGS_API_URL; -const formatDate = (timestamp: string): string => { - const date = new Date(timestamp); - const day = date.getDate(); - const month = date.toLocaleString("default", { month: "short" }); - const year = date.getFullYear().toString().slice(-2); - return `${day} ${month} ${year}`; -}; -const getUserById = async (id: number) => { - const response = await fetch(`${usersAPI}/users/${id}`); - const data = await response.json(); - return data; -} +//GraphQL queries +const GET_BLOG_BY_ID = gql` + query GetBlogById($blogId: ID!) { + blogPost(BlogID: $blogId) { + id + title + content + created_at + subtitle + image + uploadedImageLink + spotifyLink + user { + id + username + } + } + } +`; + +const GET_USER_BY_ID = gql` + query GetUserById($userId: ID!) { + user(UserID: $userId) { + id + username + } + } +`; + const Page = () => { const [isVisibleLikes, setIsVisibleLikes] = useState(true); const [isVisibleDeleteButton, setIsVisibleDeleteButton] = useState(true); const [isVisibleCommentsSection, setIsVisibleCommentsSection] = useState(true); const [loggedInUserId, setLoggedInUserId] = useState(null); - + const [isVisible, setIsVisible] = useState(false); + const pathname = usePathname(); + const blogid = pathname.split('/').pop(); + + // Fetch blog post data + const { loading: blogLoading, error: blogError, data: blogData } = useQuery(GET_BLOG_BY_ID, { + variables: { blogId: blogid }, + }); + + // Fetch user data + const { loading: userLoading, error: userError, data: userData } = useQuery(GET_USER_BY_ID, { + variables: { userId: blogData?.blogPost?.user?.id }, + }); + useEffect(() => { onAuthStateChanged(auth, async (user) => { if (!user) { setIsVisibleLikes(false); setIsVisibleDeleteButton(false); setIsVisibleCommentsSection(false); + } else { + console.log('User logged in successfully'); + try { + const userEmail = user.email; + const userIdResponse = await fetch(`${usersAPI}/users/email/${userEmail}`); + const userIdData = await userIdResponse.json(); + const userid = userIdData.ID; + setLoggedInUserId(userid); + setIsVisibleLikes(true); + setIsVisibleDeleteButton(true); + setIsVisibleCommentsSection(true); + } catch (error) { + console.error(error); + } } - else{ - console.log("User logged in successfully"); - try{ - const userEmail = user.email; - const userId = await fetch(`${usersAPI}/users/email/${userEmail}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - const data = await userId.json(); - const userid = data.ID; - setLoggedInUserId(userid); - - setIsVisibleLikes(true); - setIsVisibleDeleteButton(true); - setIsVisibleCommentsSection(true); - } catch (error) { - console.error(error); - } - } }); - } , [auth]) - - const pathname = usePathname(); - const blogid = pathname.split('/').pop(); - const [blog, setBlog] = useState(null); - const [user, setUser] = useState(null); - const [userId, setUserId] = useState(null); - const [isVisible, setIsVisible] = useState(false); + }, [auth]); - const openDelete = () => { - setIsVisible(true); - } + if (blogLoading || userLoading) return
Loading...
; + if (blogError || userError) return
Error: {blogError && blogError.message || userError && userError.message}
; - useEffect(() => { - const fetchBlog = async () => { - try { - const response = await fetch(`${blogsAPI}/blogs/${blogid}`); - if (!response.ok) { - throw new Error('Failed to fetch blog'); - } - const data = await response.json(); - console.log("Blog data", data) - setBlog(data); - setUserId(data.user_id); - const user = await getUserById(data.user_id); - setUser(user); - } catch (error) { - console.error(error); - } - }; + const blog = blogData.blogPost; + const user = userData.user; - if (blogid) { - fetchBlog(); - } - }, [blogid]); + // Function to open delete dialogue + const openDelete = () => { + setIsVisible(true); + }; - if (!blog) { - return
-
-
; - } + // Format date function + const formatDate = (timestamp : any) => { + const date = new Date(timestamp); + const day = date.getDate(); + const month = date.toLocaleString('default', { month: 'short' }); + const year = date.getFullYear().toString().slice(-2); + return `${day} ${month} ${year}`; + }; const formattedDate = formatDate(blog.created_at); @@ -109,16 +113,22 @@ const Page = () => {
- {blog.content} + {blog.content} {isVisible && setIsVisible(false)} />} - {blog.title} + {blog.title}
-

Written By: {user?.Username}

+

Written By: {user?.username}

Published On: {formattedDate}

{isVisibleLikes && } @@ -145,7 +155,6 @@ const Page = () => {
); - }; export default Page; diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 801163b..646fe92 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -3,11 +3,18 @@ import { Dosis } from "next/font/google"; import "./globals.css"; import Footer from "./components/Footer"; import { EdgeStoreProvider } from "./lib/edgestore"; +import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; + const dosis = Dosis({ subsets: ["latin"], weight: "600" } ); +const client = new ApolloClient({ + uri: process.env.NEXT_PUBLIC_GRAPHQL_API_URL, + cache: new InMemoryCache() +}); + export const metadata: Metadata = { title: "HaalSamachar", description: "Just the place you need to vent", @@ -22,9 +29,11 @@ export default function RootLayout({ + {/* */} {children} + {/* */}