implement updates page

This commit is contained in:
rocord01 2025-01-25 18:54:46 -05:00
parent 1bf731e738
commit 32924b5a1c
11 changed files with 1531 additions and 5 deletions

1119
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,11 +18,13 @@
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.468.0",
"next": "15.1.1",
"node-fetch": "^3.3.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^9.0.3",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
},

View file

@ -3,6 +3,7 @@ import Hero from '@/components/hero'
import Features from '@/components/features'
import Team from '@/components/team'
import Footer from '@/components/footer'
import Updates from '@/components/updates' // Import Blog component
export default function Home() {
return (
@ -11,6 +12,7 @@ export default function Home() {
<main>
<Hero />
<Features />
<Updates />
<Team />
</main>
<Footer />

View file

@ -0,0 +1,79 @@
import { updates } from '@/data/updates'
import Link from 'next/link'
import ReactMarkdown from 'react-markdown'
import DateDisplay from '@/components/DateDisplay'
export function generateStaticParams() {
return updates.map((post) => ({
id: post.id.toString(),
}))
}
export default function BlogPost({ params }) {
const post = updates.find((p) => p.id.toString() === params.id)
if (!post) {
return (
<div className="container py-24">
<h1 className="text-2xl font-bold">Post not found</h1>
<Link href="/" className="text-blue-500 hover:underline">
Back to home
</Link>
</div>
)
}
return (
<article className="container py-24">
<Link href="/" className="text-blue-500 hover:underline mb-8 block">
Back to home
</Link>
<header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center gap-4 mb-6 p-4 border border-gray-800 rounded-lg">
<img
src={post.author.avatar}
alt={post.author.name}
className="w-16 h-16 rounded-full"
width={64}
height={64}
/>
<div>
<h3 className="text-lg font-semibold">{post.author.name}</h3>
<p className="text-gray-400 text-sm mb-1">{post.author.role}</p>
<p className="text-gray-400 text-sm">{post.author.bio}</p>
</div>
</div>
<DateDisplay
timestamp={post.timestamp}
className="text-gray-400"
/>
</header>
<div className="prose prose-invert max-w-none">
<ReactMarkdown
components={{
h1: ({children}) => <h1 className="text-3xl font-bold mt-8 mb-4">{children}</h1>,
h2: ({children}) => <h2 className="text-2xl font-bold mt-6 mb-3">{children}</h2>,
h3: ({children}) => <h3 className="text-xl font-bold mt-4 mb-2">{children}</h3>,
p: ({children}) => <p className="mb-4">{children}</p>,
ul: ({children}) => <ul className="list-disc ml-6 mb-4">{children}</ul>,
ol: ({children}) => <ol className="list-decimal ml-6 mb-4">{children}</ol>,
li: ({children}) => <li className="mb-1">{children}</li>,
a: ({href, children}) => (
<Link href={href} className="text-blue-500 hover:underline">
{children}
</Link>
),
strong: ({children}) => <strong className="font-bold">{children}</strong>,
}}
>
{post.content}
</ReactMarkdown>
</div>
</article>
)
}

52
src/app/updates/page.jsx Normal file
View file

@ -0,0 +1,52 @@
import { updates } from '@/data/updates'
import Link from 'next/link'
import DateDisplay from '@/components/DateDisplay'
export default function UpdatesPage() {
const sortedUpdates = [...updates].sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
return (
<div className="container py-24">
<header className="mb-12">
<h1 className="text-4xl font-bold mb-4">All Updates</h1>
<Link href="/" className="text-blue-500 hover:underline">
Back to home
</Link>
</header>
<div className="grid gap-8 md:grid-cols-2">
{sortedUpdates.map(post => (
<div key={post.id} className="group p-6 border border-gray-800 rounded-lg hover:border-blue-500 transition-colors">
<div className="flex items-center gap-3 mb-4">
<img
src={post.author.avatar}
alt={post.author.name}
className="w-10 h-10 rounded-full"
width={40}
height={40}
/>
<div>
<h4 className="text-sm font-medium">{post.author.name}</h4>
<p className="text-xs text-gray-400">{post.author.role}</p>
</div>
</div>
<h3 className="text-2xl font-semibold mb-2">{post.title}</h3>
<DateDisplay
timestamp={post.timestamp}
className="text-sm text-gray-400 mb-3 block"
/>
<p className="text-gray-400 mb-4">{post.summary}</p>
<Link
href={`/updates/${post.id}`}
className="text-blue-500 hover:underline inline-flex items-center"
>
Read more
</Link>
</div>
))}
</div>
</div>
)
}

View file

@ -0,0 +1,11 @@
import { format, parseISO } from 'date-fns';
export default function DateDisplay({ timestamp, className = "" }) {
const date = parseISO(timestamp);
return (
<time dateTime={timestamp} className={className} title={format(date, 'PPpp')}>
{format(date, 'MMMM d, yyyy')}
</time>
);
}

View file

@ -20,6 +20,9 @@ export default function Header() {
<Link href="#features" className="text-gray-300 hover:text-white">
Features
</Link>
<Link href="#updates" className="text-gray-300 hover:text-white">
Updates
</Link>
<Link href="#team" className="text-gray-300 hover:text-white">
Team
</Link>

View file

@ -0,0 +1,83 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props} />
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props} />
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
{...props} />
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props} />
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props} />
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props} />
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props} />
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View file

@ -0,0 +1,56 @@
import Link from 'next/link';
import { updates } from '@/data/updates';
import Image from 'next/image';
import DateDisplay from './DateDisplay';
export default function Updates() {
const recentUpdates = [...updates]
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, 2);
return (
<section id="updates" className="container py-24">
<div className="flex justify-between items-center mb-12">
<h2 className="text-3xl font-bold">Latest Updates</h2>
<Link
href="/updates"
className="text-blue-500 hover:underline inline-flex items-center"
>
View all updates
</Link>
</div>
<div className="grid gap-8 md:grid-cols-2">
{recentUpdates.map(post => (
<div key={post.id} className="group p-6 border border-gray-800 rounded-lg hover:border-blue-500 transition-colors">
<div className="flex items-center gap-3 mb-4">
<img
src={post.author.avatar}
alt={post.author.name}
className="w-10 h-10 rounded-full"
width={40}
height={40}
/>
<div>
<h4 className="text-sm font-medium">{post.author.name}</h4>
<p className="text-xs text-gray-400">{post.author.role}</p>
</div>
</div>
<h3 className="text-2xl font-semibold mb-2">{post.title}</h3>
<DateDisplay
timestamp={post.timestamp}
className="text-sm text-gray-400 mb-3 block"
/>
<p className="text-gray-400 mb-4">{post.summary}</p>
<Link
href={`/updates/${post.id}`}
className="text-blue-500 hover:underline inline-flex items-center"
>
Read more
</Link>
</div>
))}
</div>
</section>
);
}

65
src/data/authors.js Normal file
View file

@ -0,0 +1,65 @@
export const Authors = {
CHRIS_CHROME: {
id: 'chris_chrome',
name: 'Chris Chrome',
role: 'Administrator',
ext: '1000',
discord: '@chrischrome',
avatar: '/chris.webp',
bio: 'LiteNet Project Lead and Administrator'
},
CAYDEN: {
id: 'cayden',
name: 'Cayden',
role: 'Administrator',
ext: '1001',
discord: '@freepbx',
avatar: '/cayden.webp',
bio: 'LiteNet Project Co-Lead and Administrator'
},
NICK: {
id: 'nick',
name: 'Nick',
role: 'Administrator',
ext: '1036',
discord: '@gamewell',
avatar: '/nick.webp',
bio: 'Administrator at LiteNet'
},
FAUX_LEMONS: {
id: 'faux_lemons',
name: 'Faux Lemons',
role: 'Administrator',
ext: '1011',
discord: '@faux_lemons',
avatar: '/faux_lemons.webp',
bio: 'Administrator at LiteNet'
},
ASHTON: {
id: 'ashton',
name: 'ashton',
role: 'Administrator',
ext: '1007',
discord: '@ashtoncarlson',
avatar: '/ashton.webp',
bio: 'Administrator at LiteNet'
},
MADDIX: {
id: 'maddix',
name: 'Maddix',
role: 'Moderator',
ext: '1019',
discord: '@maddix6859',
avatar: '/maddix.webp',
bio: 'Community Support and Moderation Team Member'
},
ROCORD: {
id: 'rocord',
name: 'rocord',
role: 'Moderator',
ext: '2222',
discord: '@rocord',
avatar: '/rocord.webp',
bio: 'Website Developer and VoIP Specialist'
}
};

64
src/data/updates.js Normal file
View file

@ -0,0 +1,64 @@
import { Authors } from './authors';
export const updates = [
{
id: 2,
title: "LiteNet & Snakecraft Hosting Partnership",
timestamp: "2025-01-25T18:00:00Z",
author: Authors.CHRIS_CHROME,
summary: "Exciting announcement about our new partnership with Snakecraft Hosting",
content: `
We're excited to announce that **LiteNet** is now officially sponsored by [**Snakecraft Hosting**](https://go.litenet.tel/sch-affiliate)*! 🎉
As part of this partnership, they'll be providing us with a VPS to host LiteNet, and we're incredibly grateful for their support. Snakecraft Hosting has earned our trust with their **affordable pricing**, **wide range of services**, and a **solid reputation**especially for a smaller hosting provider. We're proud to partner with them and help share what they have to offer with our community.
Don't worry—this won't change anything about LiteNet (aside from the server IP). Our commitment to providing a **safe, secure, and reliable platform** for all of you remains as strong as ever.
If you're curious about Snakecraft Hosting, feel free to check out [their website](https://go.litenet.tel/sch-affiliate)* or join their Discord server at [discord.gg/nZFQTaZWqT](https://discord.gg/nZFQTaZWqT). A big thank you to them for supporting LiteNet!
More details about the transfer will come within the next few days, so keep an eye out!
---
\**This is an affiliate link. If you use it to make a purchase, we'll receive a small commission in the form of credit for their services.*
**Note:**
* We want to clarify that we haven't been paid to make this post, and all opinions expressed are our own. Snakecraft Hosting has not influenced our views in any way.
* Any questions, comments, or concerns should be sent to the DMs of @ChrisChrome on Discord
`
},
{
id: 1,
title: "Re: LiteNet/TandmX Service",
timestamp: "2025-01-24T20:00:00Z",
author: Authors.CHRIS_CHROME,
summary: "Important announcement regarding the discontinuation of TandmX service on LiteNet",
content: `
We regret to inform everybody that as of tonight, January 24, 2025, LiteNet users will no longer be able to place or receive calls via TandmX.
## Decision Factors
This decision was not made lightly, and we understand that some users may not agree with the decision, but below is a short list of the major reasons behind the decision:
* Ever increasing restrictions as to what each independent group can do with their phone systems when they are connected to TandmX
* Recent overreach and abuse of power pertaining to another groups phone system
* An attempt to strong-arm the aforementioned group into removing their hosted line service over TandmX's offering
## Dialplan Changes
The discontinuation of TandmX service on LiteNet will also bring the following dialplan changes:
* TandmX will no longer be accessible
* AstroCom will no longer require the "00" dial prefix and can be reached by simply dialing the number directly
## Moving Forward
We understand a few of our community members rely on LiteNet to get access to TandmX, and we regret that loss of connectivity. However, we look forward to working with our community to create more bridges to other communities, be it through AstroCom, or otherwise.
Regards,
**Chris Cookman**
The LiteNet Group
`
}
];