Compare commits

...

No commits in common. "gh-pages" and "main" have entirely different histories.

42 changed files with 3889 additions and 50 deletions

10
.env.local.example Normal file
View File

@ -0,0 +1,10 @@
MINIO_ENDPOINT=
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_BUCKET=
PEERTUBE_FEED_URL=
UMAMI_ENABLED=
UMAMI_SCRIPT_URL=
UMAMI_WEBSITE_ID=

View File

@ -0,0 +1,30 @@
name: deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: "20.x"
cache: "npm"
- run: npm ci
- run: echo "COMMIT_SHORT_SHA=`git rev-parse --short HEAD`" >> $GITHUB_ENV
- run: npm run build
env:
MINIO_ENDPOINT: ${{ vars.MINIO_ENDPOINT }}
MINIO_ACCESS_KEY: ${{ vars.MINIO_ACCESS_KEY }}
MINIO_SECRET_KEY: ${{ vars.MINIO_SECRET_KEY }}
MINIO_BUCKET: ${{ vars.MINIO_BUCKET }}
PEERTUBE_FEED_URL: ${{ vars.PEERTUBE_FEED_URL }}
UMAMI_ENABLED: ${{ vars.UMAMI_ENABLED }}
UMAMI_SCRIPT_URL: ${{ vars.UMAMI_SCRIPT_URL }}
UMAMI_WEBSITE_ID: ${{ vars.UMAMI_WEBSITE_ID }}
- run: npx wrangler pages deploy out --project-name=$CF_PROJECT_NAME --branch=$GITHUB_REF_NAME
env:
CLOUDFLARE_API_TOKEN: ${{ vars.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
CF_PROJECT_NAME: ${{ vars.CF_PROJECT_NAME }}

30
.github/workflows/jobs.yaml vendored Normal file
View File

@ -0,0 +1,30 @@
name: deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- run: echo "COMMIT_SHORT_SHA=`git rev-parse --short HEAD`" >> $GITHUB_ENV
- uses: actions/setup-node@v4
with:
node-version: 21
cache: "npm"
- run: npm ci
- run: npm run build
env:
MINIO_ENDPOINT: ${{ secrets.MINIO_ENDPOINT }}
MINIO_ACCESS_KEY: ${{ secrets.MINIO_ACCESS_KEY }}
MINIO_SECRET_KEY: ${{ secrets.MINIO_SECRET_KEY }}
MINIO_BUCKET: ${{ secrets.MINIO_BUCKET }}
PEERTUBE_FEED_URL: ${{ secrets.PEERTUBE_FEED_URL }}
UMAMI_ENABLED: ${{ vars.UMAMI_ENABLED }}
UMAMI_SCRIPT_URL: ${{ vars.UMAMI_SCRIPT_URL }}
UMAMI_WEBSITE_ID: ${{ vars.UMAMI_WEBSITE_ID }}
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./out
cname: ig.rizaldy.club

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.next
node_modules
out
.devbox
.env*.local
remoteImagesForOptimization
public/nextImageExportOptimizer
public/images/next-image-export-optimizer-hashes.json

View File

View File

@ -1 +0,0 @@
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><title>404: This page could not be found</title><meta name="next-head-count" content="3"/><link rel="preload" href="/_next/static/css/25b36cfcabf50304.css" as="style" crossorigin=""/><link rel="stylesheet" href="/_next/static/css/25b36cfcabf50304.css" crossorigin="" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" crossorigin="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="https://u.rizaldy.club/umami.js" data-website-id="8dd38b8f-90e1-4df9-91ff-622d7882f05c" defer="" data-nscript="beforeInteractive" crossorigin=""></script><script src="/_next/static/chunks/webpack-ee7e63bc15b31913.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/framework-1e817f2a1c5c711b.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/main-72cd581c1e9bd837.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/pages/_app-4f9a8d5bb2887a9b.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/pages/_error-b6491f42fb2263bb.js" defer="" crossorigin=""></script><script src="/_next/static/yOMHMNqsMpN-23tJUJ2XT/_buildManifest.js" defer="" crossorigin=""></script><script src="/_next/static/yOMHMNqsMpN-23tJUJ2XT/_ssgManifest.js" defer="" crossorigin=""></script></head><body><div id="__next"><div class="min-h-screen bg-white dark:bg-black dark:text-neutral-200"><nav class="flex py-2 border-b dark:border-neutral-800 hover:opacity-70"><div class="w-3/12 md:w-full lg:w-7/12 md:mx-11 lg:mx-auto"><div class="md:w-2/12"><a href="/"><img alt="not instagram™" src="https://s3.rizaldy.club/0x0/d43840c5ee4ec66a3bee3c6e2.png" class="w-full"/></a></div></div></nav><div class="flex items-center justify-between w-full my-5"><div class="md:w-11/12 lg:w-7/12 mx-auto"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div style="line-height:48px"><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:28px">This page could not be found<!-- -->.</h2></div></div></div></div></div><footer class="flex py-4 text-center md:mt-10 mt-5"><div class="md:w-7/12 mx-auto font-semibold"><div class="md:text-sm mb-10"><a class="md:mx-4 mb-1 md:inline block hover:opacity-70" href="/static/about">About</a><a class="md:mx-4 mb-1 md:inline block hover:opacity-70" href="https://rizaldy.club">Blog</a><a class="md:mx-4 mb-1 md:inline block hover:opacity-70" href="https://edgy.social/@rizaldy">Mastodon</a><a class="md:mx-4 mb-1 md:inline block hover:opacity-70" href="https://bsky.app/profile/rizaldy.club">Bluesky</a><a class="mx-4 hover:opacity-70" href="https://github.com/faultables/ig.rizaldy.club">Source Code</a></div><div class="text-sm font-normal text-neutral-400"><p>© <!-- -->MMXXIV<!-- --> <a class="text-neutral-600 dark:text-neutral-200 hover:opacity-70" target="_blank" rel="noopener noreferer" href="https://github.com/faultables">faultables</a> <!-- -->• All media is licensed under<!-- --> <a class="underline hover:opacity-70" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> <!-- -->unless stated otherwise •<!-- --> <a class="text-neutral-600 dark:text-neutral-200 hover:opacity-70" target="_blank" rel="noopener noreferer" href="https://github.com/faultables/ig.rizaldy.club/commit/1102852">1102852</a></p></div></div></footer></div></div><script id="__NEXT_DATA__" type="application/json" crossorigin="">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"yOMHMNqsMpN-23tJUJ2XT","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>

1
CNAME
View File

@ -1 +0,0 @@
ig.rizaldy.club

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{1981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(8435)}])}},function(n){n.O(0,[774,888,179],function(){return n(n.s=1981)}),_N_E=n.O()}]);

View File

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[405],{5557:function(e,l,t){(window.__NEXT_P=window.__NEXT_P||[]).push(["/",function(){return t(5171)}])},5171:function(e,l,t){"use strict";t.r(l),t.d(l,{__N_SSG:function(){return p},default:function(){return f}});var r=t(5893),s=t(7294),a=t(7205);let n=(e,l)=>e===l?"border-t dark:border-neutral-400":"";var o=e=>{let{activeTab:l,setActiveTab:t}=e;return(0,r.jsxs)("div",{className:"flex items-center justify-center text-center gap-5 border-t mb-4 dark:border-neutral-800",children:[(0,r.jsx)("button",{onClick:()=>t("photos"),className:"uppercase tracking-tight font-semibold text-sm border-black pt-3 px-3 ".concat(n("photos",l)),children:"Photos"}),(0,r.jsx)("button",{onClick:()=>t("videos"),className:"uppercase tracking-tight font-semibold text-sm pt-3 px-3 border-black ".concat(n("videos",l)),children:"Videos"})]})},d=e=>{let{media:l,type:t,setOverlayContent:s,currentIndex:a,limitPerPage:n}=e;return(0,r.jsx)("main",{className:"grid grid-cols-3 gap-1",children:l.slice(a,n).map(e=>{let{id:l,url:a,previewPath:n,embedPath:o=a}=e;return(0,r.jsx)("a",{href:a,target:"_blank",rel:"noopener noreferer",className:"hover:opacity-70",onClick:e=>{e.preventDefault(),s({url:o,type:t})},children:(0,r.jsx)("img",{alt:a,loading:"lazy",src:n,className:"aspect-".concat(t," bg-neutral-100 dark:bg-neutral-900")})},l)})})},i=e=>{let{profile:l,totalPosts:t}=e;return(0,r.jsxs)("div",{className:"flex flex-warp",children:[(0,r.jsx)("div",{className:"lg:w-4/12 w-3/12 md:py-10 ml-3 md:ml-0 lg:px-20 md:px-10",children:(0,r.jsx)("img",{loading:"lazy",alt:l.display_name,src:l.avatar,className:"w-full rounded-full mx-auto border bg-neutral-100 border-gray-200 p-1 dark:border-neutral-800 dark:bg-black"})}),(0,r.jsxs)("div",{className:"lg:w-9/12 md:w-10/12 mb-5 md:p-5 ml-5",children:[(0,r.jsxs)("div",{className:"md:flex items-center",children:[(0,r.jsx)("h2",{className:"text-2xl font-semibold",children:l.username}),(0,r.jsxs)("div",{className:"md:ml-4 my-4",children:[(0,r.jsx)("a",{className:"rounded-md bg-gray-100 px-5 font-semibold py-2 md:ml-2 text-sm leading-relaxed dark:bg-neutral-800 hover:opacity-70",href:l.follow_url,children:"Follow"}),(0,r.jsx)("a",{className:"rounded-md bg-gray-100 px-5 font-semibold py-2 ml-2 text-sm leading-relaxed dark:bg-neutral-800 hover:opacity-70",href:l.message_url,children:"Message"})]})]}),(0,r.jsx)("div",{className:"md:my-5 my-3 font-bold text-sm",children:(0,r.jsxs)("p",{children:[t," posts"]})}),(0,r.jsx)("p",{className:"font-bold mb-1",children:l.display_name}),(0,r.jsx)("p",{children:l.about}),(0,r.jsx)("p",{className:"leading-loose font-semibold text-blue-900 dark:text-blue-200",children:(0,r.jsx)("a",{target:"_blank",className:"hover:underline",rel:"noopener noreferer",href:"https://".concat(l.link),children:l.link})})]})]})};let c=e=>{let{overlayContent:l}=e;return(null==l?void 0:l.type)===a.oZ.PHOTOS?(0,r.jsx)("img",{alt:null==l?void 0:l.url,src:null==l?void 0:l.url,className:"z-20 cursor-default"}):(null==l?void 0:l.type)===a.oZ.VIDEOS?(0,r.jsx)("iframe",{className:"md:w-6/12 w-full h-1/2",allow:"fullscreen",sandbox:"allow-same-origin allow-scripts allow-popups",src:null==l?void 0:l.url}):void 0};var m=e=>{let{overlayContent:l,closeOverlay:t}=e,s=(null==l?void 0:l.type)!==void 0;return(0,r.jsxs)("div",{onClick:t,className:"".concat(s?"bg-neutral-800/95 fixed w-full h-full left-0 top-0 cursor-pointer z-30":""),children:[s?(0,r.jsx)("p",{onClick:t,className:"fixed right-0 bottom-0 md:mx-5 my-5 text-white rounded text-sm text-center w-full z-30",children:'Click anywhere or press "Escape" to close'}):null,(0,r.jsx)("div",{className:"flex justify-center items-center ".concat(s?"h-full w-full":"h-0 w-0"),children:(0,r.jsx)(c,{overlayContent:l})})]})},u=t(356);let x=e=>{let{activeTab:l,photos:t,videos:s,...n}=e;return l===a.oZ.PHOTOS?(0,r.jsx)(d,{media:t,type:l,...n}):l===a.oZ.VIDEOS?(0,r.jsx)(d,{media:s,type:l,...n}):(0,r.jsx)("div",{className:"text-center pt-10 font-bold text-2xl",children:"nice try"})};var p=!0,f=e=>{let{photos:l,videos:t,totalPosts:n}=e,[d,c]=(0,s.useState)(a.oZ.PHOTOS),[p,f]=(0,s.useState)(0),[h,b]=(0,s.useState)(null),v=()=>b(null),g=e=>{let{keyCode:l}=e;l===a.O_&&v()};return(0,s.useEffect)(()=>(window.addEventListener("keydown",g),()=>{window.removeEventListener("keydown",g)}),[g]),(0,r.jsxs)(s.Fragment,{children:[(0,r.jsx)(i,{profile:u.N5,totalPosts:n}),(0,r.jsx)(o,{activeTab:d,setActiveTab:c}),(0,r.jsx)(m,{overlayContent:h,closeOverlay:v}),(0,r.jsx)(x,{currentIndex:p,setCurrentIndex:f,limitPerPage:a.Zv,activeTab:d,setOverlayContent:b,photos:l,videos:t})]})}}},function(e){e.O(0,[774,888,179],function(){return e(e.s=5557)}),_N_E=e.O()}]);

View File

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[664],{1556:function(a,e,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/static/about",function(){return n(3414)}])},3414:function(a,e,n){"use strict";n.r(e);var i=n(5893);e.default=()=>(0,i.jsx)("div",{className:"p-2",children:(0,i.jsxs)("div",{className:"md:border p-5 md:w-8/12 md:shadow md:rotate-[-0.5deg] mx-auto text-neutral-600 dark:text-neutral-200 dark:border-neutral-800",children:[(0,i.jsx)("h2",{className:"font-bold text-3xl leading-loose mb-2 text-black dark:text-neutral-200",children:"Notes from @faultables"}),(0,i.jsxs)("p",{className:"mb-6",children:["Sebelumnya saya menjalankan ",(0,i.jsx)("em",{children:"instance"})," ",(0,i.jsx)("a",{href:"https://pixelfed.org",className:"underline hover:opacity-70",target:"_blank",rel:"noopener noreferer",children:"Pixelfed"})," ",'selama bertahun-tahun sebagai alternatif dari sosial media mainstream yang dijalankan oleh "big co". Pixelfed masih dalam tahap pengembangan, sehingga, adanya bug dan masalah acak lainnya adalah hal yang wajar.']}),(0,i.jsxs)("p",{className:"mb-6",children:["Disamping itu, Pixelfed sangat menjanjikan: menggunakan protokol"," ",(0,i.jsx)("a",{href:"https://www.w3.org/TR/activitypub/",className:"underline hover:opacity-70",target:"_blank",rel:"noopener noreferer",children:"ActivityPub"})," ",'sehingga bisa "berfederasi" dengan ',(0,i.jsx)("em",{children:"instance"})," lain, dan yang paling penting adalah"," ",(0,i.jsx)("a",{href:"https://github.com/pixelfed/pixelfed",classname:"underline hover:opacity-70",target:"_blank",rel:"noopener noreferer",children:"bersumber kode terbuka"})," ",(0,i.jsx)("strong",{children:"dan"}),' dikembangkan murni oleh komunitas. Pixelfed mendukung fitur standar untuk bersosial media seperti memperbaharui status, mengikuti pengguna, mengirim komentar, menyukai, intinya fitur bersosial apapun yang sudah menjadi mainstream. Meskipun saya sudah memiliki akun sosial media lainnya di "universe"'," ",(0,i.jsx)("a",{href:"https://joinmastodon.org",className:"underline hover:opacity-70",target:"_blank",rel:"noopener noreferer",children:"Mastodon"}),", saya memilih Pixelfed murni hanya untuk berbagi media dalam bentuk foto saja."]}),(0,i.jsx)("p",{className:"mb-6",children:"Tapi, ya, saya tidak menggunakan Pixelfed sesering itu. Saya melakukan optimasi gambar secara manual berikut menghapus metadata exif dan memotong gambar ke 1024px tanpa menggunakan fitur built-in (crop) karena terkadang fiturnya tidak berjalan sesuatu dengan yang harapkan."}),(0,i.jsx)("p",{className:"mb-6",children:'Secara teknis, menjalankan 2 aplikasi (Laravel & Horizon) plus MySQL bukanlah hal yang sulit dan mahal, namun bagaimanapun, saya tidak menggunakan "fitur sosial" yang ada di Pixelfed karena itulah yang saya inginkan sehingga terkesan seperti berlebihan.'}),(0,i.jsx)("p",{className:"mb-6",children:"Jika kamu seorang fotografer, kamu bisa mencoba Pixelfed. Kamu juga bisa berinteraksi dengan komunitas di jaringan yang sama—selama menggunakan protokol ActivityPub—karena Pixelfed adalah jaringan federasi!"}),(0,i.jsx)("p",{className:"mb-6",children:"Bagaimanapun, pilihan ini bukanlah pendekatan yang terbaik. Tapi setidaknya, ini tidak berlebihan, khususnya untuk saat ini."}),(0,i.jsx)("br",{}),(0,i.jsx)("p",{className:"mb-6",children:(0,i.jsx)("a",{href:"https://github.com/faultables",className:"hover:opacity-70",target:"_blank",rel:"noopener noreferer",children:"— faultables"})})]})})}},function(a){a.O(0,[774,888,179],function(){return a(a.s=1556)}),_N_E=a.O()}]);

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
!function(){"use strict";var e,n,r,t,o={},u={};function i(e){var n=u[e];if(void 0!==n)return n.exports;var r=u[e]={exports:{}},t=!0;try{o[e](r,r.exports,i),t=!1}finally{t&&delete u[e]}return r.exports}i.m=o,e=[],i.O=function(n,r,t,o){if(r){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[r,t,o];return}for(var f=1/0,u=0;u<e.length;u++){for(var r=e[u][0],t=e[u][1],o=e[u][2],c=!0,l=0;l<r.length;l++)f>=o&&Object.keys(i.O).every(function(e){return i.O[e](r[l])})?r.splice(l--,1):(c=!1,o<f&&(f=o));if(c){e.splice(u--,1);var a=t();void 0!==a&&(n=a)}}return n},i.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(n,{a:n}),n},i.d=function(e,n){for(var r in n)i.o(n,r)&&!i.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},i.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),i.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.p="/_next/",n={272:0},i.O.j=function(e){return 0===n[e]},r=function(e,r){var t,o,u=r[0],f=r[1],c=r[2],l=0;if(u.some(function(e){return 0!==n[e]})){for(t in f)i.o(f,t)&&(i.m[t]=f[t]);if(c)var a=c(i)}for(e&&e(r);l<u.length;l++)o=u[l],i.o(n,o)&&n[o]&&n[o][0](),n[o]=0;return i.O(a)},(t=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))}();

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":["static/chunks/pages/index-c91f1d8faf4d92e1.js"],"/_error":["static/chunks/pages/_error-b6491f42fb2263bb.js"],"/static/about":["static/chunks/pages/static/about-30f1ad42665a60b3.js"],sortedPages:["/","/_app","/_error","/static/about"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

View File

@ -1 +0,0 @@
self.__SSG_MANIFEST=new Set(["\u002F"]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()

42
components/Feed.js Normal file
View File

@ -0,0 +1,42 @@
import ExportedImage from "next-image-export-optimizer";
const Feed = ({
media,
type,
setOverlayContent,
currentIndex,
limitPerPage,
}) => (
<main className="grid grid-cols-3 gap-1">
{media
.slice(currentIndex, limitPerPage)
.map(({ id, url, previewPath, embedPath = url }) => (
<a
key={id}
href={url}
target="_blank"
rel="noopener noreferer"
className="hover:opacity-70"
onClick={(e) => {
e.preventDefault();
setOverlayContent({
url: embedPath,
type,
});
}}
>
<ExportedImage
alt={url}
loading="lazy"
src={previewPath}
width="512"
height="512"
className={`aspect-${type} bg-neutral-100 dark:bg-neutral-900`}
/>
</a>
))}
</main>
);
export default Feed;

53
components/Footer.js Normal file
View File

@ -0,0 +1,53 @@
import Link from "next/link";
import { YEAR_TO_BUMP } from "../constants";
const Footer = ({ license, links, repo, commitID }) => (
<footer className="flex py-4 text-center md:mt-10 mt-5">
<div className="md:w-7/12 mx-auto font-semibold">
<div className="md:text-sm mb-10">
{links.map(({ url, label }) => (
<Link
key={url + label}
href={url}
className="md:mx-4 mb-1 md:inline block hover:opacity-70"
>
{label}
</Link>
))}
<Link href={repo} className="mx-4 hover:opacity-70">
Source Code
</Link>
</div>
<div className="text-sm font-normal text-neutral-400">
<p>
&copy; {YEAR_TO_BUMP}{" "}
<Link
className="text-neutral-600 dark:text-neutral-200 hover:opacity-70"
target="_blank"
rel="noopener noreferer"
href="https://faultables.net"
>
faultables
</Link>{" "}
All media is licensed under{" "}
<Link className="underline hover:opacity-70" href={license.url}>
{license.name}
</Link>{" "}
unless stated otherwise {" "}
<Link
className="text-neutral-600 dark:text-neutral-200 hover:opacity-70"
target="_blank"
rel="noopener noreferer"
href={`${repo}/commit/${commitID}`}
>
{commitID}
</Link>
</p>
</div>
</div>
</footer>
);
export default Footer;

28
components/Navbar.js Normal file
View File

@ -0,0 +1,28 @@
import Link from "next/link";
import ExportedImage from "next-image-export-optimizer";
const Navbar = ({ name, logo }) => (
<nav className="flex py-2 border-b dark:border-neutral-800 hover:opacity-70">
<div className="w-3/12 md:w-full lg:w-7/12 md:mx-11 lg:mx-auto">
<div className="md:w-2/12">
<Link href="/">
{logo ? (
<ExportedImage
alt={name}
src={logo}
width={224}
height={56}
className="w-full"
/>
) : (
<h1 className="font-bold leading-relaxed tracking-tight text-2xl text-neutral-800">
{name}
</h1>
)}
</Link>
</div>
</div>
</nav>
);
export default Navbar;

66
components/Overlay.js Normal file
View File

@ -0,0 +1,66 @@
import { MEDIA_TYPE } from "../constants";
const OverlayContent = ({ overlayContent }) => {
if (overlayContent?.type === MEDIA_TYPE.PHOTOS) {
return (
<img
alt={overlayContent?.url}
src={overlayContent?.url}
className="z-20 cursor-default"
/>
);
} else if (overlayContent?.type === MEDIA_TYPE.VIDEOS) {
return (
<iframe
className="md:w-6/12 w-full h-1/2"
allow="fullscreen"
sandbox="allow-same-origin allow-scripts allow-popups"
src={overlayContent?.url}
></iframe>
);
}
};
const Overlay = ({ overlayContent, closeOverlay }) => {
const isOverlayOpen = overlayContent?.type !== undefined;
return (
<div
onClick={closeOverlay}
className={`${
isOverlayOpen
? "bg-neutral-800/95 fixed w-full h-full left-0 top-0 cursor-pointer z-30"
: ""
}`}
>
{isOverlayOpen ? (
<p
onClick={closeOverlay}
className="fixed right-0 bottom-0 md:mx-5 my-5 text-white rounded text-sm text-center w-full z-30 md:bottom-10 px-2 leading-relaxed"
>
Click anywhere or press "Escape" to close
<span className="px-2">|</span>
<a
className="underline"
target="_blank"
rel="noreferer noopener"
href={overlayContent?.url}
>
Click here
</a>{" "}
to see the raw media
</p>
) : null}
<div
className={`flex justify-center items-center ${
isOverlayOpen ? "h-full w-full" : "h-0 w-0"
}`}
>
<OverlayContent overlayContent={overlayContent} />
</div>
</div>
);
};
export default Overlay;

52
components/Profile.js Normal file
View File

@ -0,0 +1,52 @@
import ExportedImage from "next-image-export-optimizer";
const Profile = ({ profile, totalPosts }) => (
<div className="flex flex-warp">
<div className="lg:w-4/12 w-3/12 md:py-10 ml-3 md:ml-0 lg:px-20 md:px-10">
<ExportedImage
loading="lazy"
alt={profile.display_name}
src={profile.avatar}
width={250}
height={250}
className="w-full rounded-full mx-auto border bg-neutral-100 border-gray-200 p-1 dark:border-neutral-800 dark:bg-black"
/>
</div>
<div className="lg:w-9/12 md:w-10/12 mb-5 md:p-5 ml-5">
<div className="md:flex items-center">
<h2 className="text-2xl font-semibold">{profile.username}</h2>
<div className="md:ml-4 my-4">
<a
className="rounded-md bg-gray-100 px-5 font-semibold py-2 md:ml-2 text-sm leading-relaxed dark:bg-neutral-800 hover:opacity-70"
href={profile.follow_url}
>
Follow
</a>
<a
className="rounded-md bg-gray-100 px-5 font-semibold py-2 ml-2 text-sm leading-relaxed dark:bg-neutral-800 hover:opacity-70"
href={profile.message_url}
>
Message
</a>
</div>
</div>
<div className="md:my-5 my-3 font-bold text-sm">
<p>{totalPosts} posts</p>
</div>
<p className="font-bold mb-1">{profile.display_name}</p>
<p>{profile.about}</p>
<p className="leading-loose font-semibold text-blue-900 dark:text-blue-200">
<a
target="_blank"
className="hover:underline"
rel="noopener noreferer"
href={`https://${profile.link}`}
>
{profile.link}
</a>
</p>
</div>
</div>
);
export default Profile;

27
components/Tab.js Normal file
View File

@ -0,0 +1,27 @@
const isActiveTab = (currentTab, activeTab) =>
currentTab === activeTab ? "border-t dark:border-neutral-400" : "";
const Tab = ({ activeTab, setActiveTab }) => (
<div className="flex items-center justify-center text-center gap-5 border-t mb-4 dark:border-neutral-800">
<button
onClick={() => setActiveTab("photos")}
className={`uppercase tracking-tight font-semibold text-sm border-black pt-3 px-3 ${isActiveTab(
"photos",
activeTab
)}`}
>
Photos
</button>
<button
onClick={() => setActiveTab("videos")}
className={`uppercase tracking-tight font-semibold text-sm pt-3 px-3 border-black ${isActiveTab(
"videos",
activeTab
)}`}
>
Videos
</button>
</div>
);
export default Tab;

36
config.json Normal file
View File

@ -0,0 +1,36 @@
{
"navbar": {
"logotype": "not instagram™",
"logo": "https://s3.rizaldy.club/0x0/d43840c5ee4ec66a3bee3c6e2.png"
},
"profile": {
"avatar": "https://s3.rizaldy.club/0x0/IMG_6279.JPG",
"username":"faultables",
"follow_url": "https://edgy.social/@rizaldy",
"message_url": "mailto:rizaldy@duck.com",
"display_name": "rizaldy",
"about": "SRE, DevOps, and everything in between",
"link": "rizaldy.club"
},
"footer": {
"repo": "https://forge.edgy.social/rizaldy/ig.rizaldy.club",
"links": [
{
"label": "About",
"url": "/static/about"
},
{
"label": "Mastodon",
"url": "https://edgy.social/@rizaldy"
},
{
"label": "Bluesky",
"url": "https://bsky.app/profile/rizaldy.club"
}
],
"license": {
"name": "CC BY-NC-SA 4.0",
"url": "https://creativecommons.org/licenses/by-nc-sa/4.0/"
}
}
}

8
constants.js Normal file
View File

@ -0,0 +1,8 @@
export const MEDIA_PER_PAGE = 40;
export const MEDIA_TYPE = {
PHOTOS: "photos",
VIDEOS: "videos",
};
export const YEAR_TO_BUMP = "MMXXIV";
export const ESCAPE_KEY = 27;

3
devbox.json Normal file
View File

@ -0,0 +1,3 @@
{
"packages": ["nodejs@latest"]
}

25
devbox.lock Normal file
View File

@ -0,0 +1,25 @@
{
"lockfile_version": "1",
"packages": {
"nodejs@latest": {
"last_modified": "2024-01-14T03:55:27Z",
"resolved": "github:NixOS/nixpkgs/dd5621df6dcb90122b50da5ec31c411a0de3e538#nodejs_21",
"source": "devbox-search",
"version": "21.5.0",
"systems": {
"aarch64-darwin": {
"store_path": "/nix/store/ybpqk26vz7k9grapsgx0sd900s0sp4sa-nodejs-21.5.0"
},
"aarch64-linux": {
"store_path": "/nix/store/brnzb5xxgdx6bbicygz83ybi5inqp09v-nodejs-21.5.0"
},
"x86_64-darwin": {
"store_path": "/nix/store/yvgnx3lj8am9mqn30yr09sb4ia7qy3w8-nodejs-21.5.0"
},
"x86_64-linux": {
"store_path": "/nix/store/nxfirpvaycr7wqzwl6wqifpdrqn7is7x-nodejs-21.5.0"
}
}
}
}
}

File diff suppressed because one or more lines are too long

19
next.config.js Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
output: "export",
images: {
loader: "custom",
deviceSizes: [1080, 1200, 1920],
imageSizes: [128, 256, 384],
},
transpilePackages: ["next-image-export-optimizer"],
env: {
COMMIT_SHORT_SHA: process.env.COMMIT_SHORT_SHA || "HEAD",
nextImageExportOptimizer_imageFolderPath: "public/images",
nextImageExportOptimizer_exportFolderPath: "out",
nextImageExportOptimizer_quality: "90",
nextImageExportOptimizer_storePicturesInWEBP: "true",
nextImageExportOptimizer_exportFolderName: "nextImageExportOptimizer",
nextImageExportOptimizer_generateAndUseBlurImages: "true",
nextImageExportOptimizer_remoteImageCacheTTL: "31536000",
},
};

3092
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"scripts": {
"dev": "next dev",
"build": "next build && next-image-export-optimizer",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"minio": "^7.1.3",
"next": "^14.0.4",
"next-image-export-optimizer": "^1.12.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"xml2json": "^0.12.0"
},
"devDependencies": {
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1"
}
}

38
pages/_app.js Normal file
View File

@ -0,0 +1,38 @@
import "../styles/globals.css";
import Script from "next/script";
import Navbar from "../components/Navbar";
import Footer from "../components/Footer";
import config from "../config.json";
const Content = ({ children }) => (
<div className="flex items-center justify-between w-full my-5">
<div className="md:w-11/12 lg:w-7/12 mx-auto">{children}</div>
</div>
);
const App = ({ Component, pageProps }) => (
<div className='min-h-screen bg-white dark:bg-black dark:text-neutral-200'>
<Navbar name={config.navbar.logotype} logo={config.navbar.logo} />
<Content>
<Component {...pageProps} />
</Content>
<Footer
repo={config.footer.repo}
links={config.footer.links}
license={config.footer.license}
commitID={process.env.COMMIT_SHORT_SHA || "HEAD"}
/>
{process.env.UMAMI_ENABLED ? (
<Script
strategy="beforeInteractive"
src={process.env.UMAMI_SCRIPT_URL}
data-website-id={process.env.UMAMI_WEBSITE_ID}
/>
) : null}
</div>
);
export default App;

15
pages/_document.js Normal file
View File

@ -0,0 +1,15 @@
import { Html, Head, Main, NextScript } from "next/document";
const Document = () => {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
};
export default Document;

114
pages/index.js Normal file
View File

@ -0,0 +1,114 @@
import xml2json from "xml2json";
import { Client } from "minio";
import { Fragment, useState, useEffect } from "react";
import { ESCAPE_KEY, MEDIA_PER_PAGE, MEDIA_TYPE } from "../constants";
import Tab from "../components/Tab";
import Feed from "../components/Feed";
import Profile from "../components/Profile";
import Overlay from "../components/Overlay";
import config from "../config.json";
const Content = ({ activeTab, photos, videos, ...props }) => {
if (activeTab === MEDIA_TYPE.PHOTOS) {
return <Feed media={photos} type={activeTab} {...props} />;
} else if (activeTab === MEDIA_TYPE.VIDEOS) {
return <Feed media={videos} type={activeTab} {...props} />;
} else {
return <div className="text-center pt-10 font-bold text-2xl">nice try</div>;
}
};
const IndexPage = ({ photos, videos, totalPosts }) => {
const [activeTab, setActiveTab] = useState(MEDIA_TYPE.PHOTOS);
const [currentIndex, setCurrentIndex] = useState(0);
const [overlayContent, setOverlayContent] = useState(null);
const closeOverlay = () => setOverlayContent(null);
const handleClose = ({ keyCode }) => {
if (keyCode === ESCAPE_KEY) {
closeOverlay();
}
};
useEffect(() => {
window.addEventListener("keydown", handleClose);
return () => {
window.removeEventListener("keydown", handleClose);
};
}, [handleClose]);
return (
<Fragment>
<Profile profile={config.profile} totalPosts={totalPosts} />
<Tab activeTab={activeTab} setActiveTab={setActiveTab} />
<Overlay overlayContent={overlayContent} closeOverlay={closeOverlay} />
<Content
currentIndex={currentIndex}
setCurrentIndex={setCurrentIndex}
limitPerPage={MEDIA_PER_PAGE}
activeTab={activeTab}
setOverlayContent={setOverlayContent}
photos={photos}
videos={videos}
/>
</Fragment>
);
};
export async function getStaticProps() {
const minioEndpoint = process.env.MINIO_ENDPOINT;
const minioBucket = process.env.MINIO_BUCKET;
const mc = new Client({
endPoint: minioEndpoint,
accessKey: process.env.MINIO_ACCESS_KEY,
secretKey: process.env.MINIO_SECRET_KEY,
});
const minioObjects = await new Promise((resolve, reject) => {
const data = [];
const stream = mc.listObjectsV2(minioBucket, "photos", true, "");
stream.on("data", (obj) => data.push(obj.name));
stream.on("error", reject);
stream.on("end", () => {
resolve(data);
});
});
const photos = minioObjects.map((id) => ({
id,
url: `https://${minioEndpoint}/${minioBucket}/${id}`,
previewPath: `https://${minioEndpoint}/${minioBucket}/${id}`,
})).sort().reverse();
const getPeertubeFeeds = await fetch(process.env.PEERTUBE_FEED_URL);
const peertubeFeeds = await getPeertubeFeeds.text();
const peertubeFeedsJSON = xml2json.toJson(peertubeFeeds, { object: true });
const peertubeItems = peertubeFeedsJSON.rss.channel.item;
const videos = peertubeItems.map((item) => ({
id: item.guid,
url: item["media:embed"].url,
previewPath: item["media:thumbnail"][0].url,
}));
return {
props: {
config,
photos,
videos,
totalPosts: photos.length + videos.length,
},
};
}
export default IndexPage;

93
pages/static/about.js Normal file
View File

@ -0,0 +1,93 @@
const About = () => (
<div className="p-2">
<div className="md:border p-5 md:w-8/12 md:shadow md:rotate-[-0.5deg] mx-auto text-neutral-600 dark:text-neutral-200 dark:border-neutral-800">
<h2 className="font-bold text-3xl leading-loose mb-2 text-black dark:text-neutral-200">
Notes from @faultables
</h2>
<p className="mb-6">
Sebelumnya saya menjalankan <em>instance</em>{" "}
<a
href="https://pixelfed.org"
className="underline hover:opacity-70"
target="_blank"
rel="noopener noreferer"
>
Pixelfed
</a>{" "}
selama bertahun-tahun sebagai alternatif dari sosial media mainstream
yang dijalankan oleh "big co". Pixelfed masih dalam tahap pengembangan,
sehingga, adanya bug dan masalah acak lainnya adalah hal yang wajar.
</p>
<p className="mb-6">
Disamping itu, Pixelfed sangat menjanjikan: menggunakan protokol{" "}
<a
href="https://www.w3.org/TR/activitypub/"
className="underline hover:opacity-70"
target="_blank"
rel="noopener noreferer"
>
ActivityPub
</a>{" "}
sehingga bisa "berfederasi" dengan <em>instance</em> lain, dan yang
paling penting adalah{" "}
<a
href="https://github.com/pixelfed/pixelfed"
classname="underline hover:opacity-70"
target="_blank"
rel="noopener noreferer"
>
bersumber kode terbuka
</a>{" "}
<strong>dan</strong> dikembangkan murni oleh komunitas. Pixelfed
mendukung fitur standar untuk bersosial media seperti memperbaharui
status, mengikuti pengguna, mengirim komentar, menyukai, intinya fitur
bersosial apapun yang sudah menjadi mainstream. Meskipun saya sudah
memiliki akun sosial media lainnya di "universe"{" "}
<a
href="https://joinmastodon.org"
className="underline hover:opacity-70"
target="_blank"
rel="noopener noreferer"
>
Mastodon
</a>
, saya memilih Pixelfed murni hanya untuk berbagi media dalam bentuk
foto saja.
</p>
<p className="mb-6">
Tapi, ya, saya tidak menggunakan Pixelfed sesering itu. Saya melakukan
optimasi gambar secara manual berikut menghapus metadata exif dan
memotong gambar ke 1024px tanpa menggunakan fitur built-in (crop) karena
terkadang fiturnya tidak berjalan sesuatu dengan yang harapkan.
</p>
<p className="mb-6">
Secara teknis, menjalankan 2 aplikasi (Laravel & Horizon) plus MySQL
bukanlah hal yang sulit dan mahal, namun bagaimanapun, saya tidak
menggunakan "fitur sosial" yang ada di Pixelfed karena itulah yang saya
inginkan sehingga terkesan seperti berlebihan.
</p>
<p className="mb-6">
Jika kamu seorang fotografer, kamu bisa mencoba Pixelfed. Kamu juga bisa
berinteraksi dengan komunitas di jaringan yang samaselama menggunakan
protokol ActivityPubkarena Pixelfed adalah jaringan federasi!
</p>
<p className="mb-6">
Bagaimanapun, pilihan ini bukanlah pendekatan yang terbaik. Tapi
setidaknya, ini tidak berlebihan, khususnya untuk saat ini.
</p>
<br />
<p className="mb-6">
<a
href="https://rizaldy.club"
className="hover:opacity-70"
target="_blank"
rel="noopener noreferer"
>
faultables
</a>
</p>
</div>
</div>
);
export default About;

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

42
remoteOptimizedImages.js Normal file
View File

@ -0,0 +1,42 @@
const config = require("./config.json");
const minio = require("minio");
const xml2json = require("xml2json");
const minioEndpoint = process.env.MINIO_ENDPOINT;
const minioBucket = process.env.MINIO_BUCKET;
const mc = new minio.Client({
endPoint: minioEndpoint,
accessKey: process.env.MINIO_ACCESS_KEY,
secretKey: process.env.MINIO_SECRET_KEY,
});
const minioObjects = new Promise((resolve, reject) => {
const data = [];
const stream = mc.listObjectsV2(minioBucket, "photos", true, "");
stream.on("data", (obj) => data.push(obj.name));
stream.on("error", reject);
stream.on("end", () => {
resolve(data);
});
}).then((data) => {
return data.map((id) => `https://${minioEndpoint}/${minioBucket}/${id}`);
});
const getPeertubeFeeds = fetch(process.env.PEERTUBE_FEED_URL)
.then((res) => res.text())
.then((payload) => {
const peertubeFeedsJSON = xml2json.toJson(payload, { object: true });
const peertubeItems = peertubeFeedsJSON.rss.channel.item;
const videos = peertubeItems.map((item) => item["media:thumbnail"][0].url);
return videos;
});
module.exports = Promise.all([minioObjects, getPeertubeFeeds]).then((data) => {
return [...data[0], ...data[1], config.navbar.logo, config.profile.avatar];
});

File diff suppressed because one or more lines are too long

22
styles/globals.css Normal file
View File

@ -0,0 +1,22 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background-color: #ffffff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000000;
}
}
/* Somehow tailwind doesn't include this??? */
.aspect-photos {
aspect-ratio: 1 / 1;
}
.aspect-videos {
aspect-ratio: 16 / 9;
}

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
mode: "jit",
content: ["./pages/**/*.js", "./components/**/*.js"],
theme: {
extend: {},
},
plugins: [],
};