mirror of
https://github.com/BradNut/svelteKitForBeginners
synced 2025-09-08 17:40:24 +00:00
Fetch tweets in an api and design how it shows up in the feed.
This commit is contained in:
parent
cf52758c94
commit
6aebae4688
8 changed files with 351 additions and 1 deletions
219
src/components/tweet.svelte
Normal file
219
src/components/tweet.svelte
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { fade, fly } from 'svelte/transition'
|
||||||
|
|
||||||
|
import Icon from '$root/components/icon.svelte'
|
||||||
|
import type { TweetType } from '$root/types'
|
||||||
|
|
||||||
|
export let tweet: TweetType
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<article class="tweet-container" transition:fade>
|
||||||
|
<a class="avatar" href="/home/profile/{tweet.name}">
|
||||||
|
<img
|
||||||
|
width="140"
|
||||||
|
height="140"
|
||||||
|
src={tweet.avatar}
|
||||||
|
alt={tweet.name}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="tweet-details">
|
||||||
|
<div>
|
||||||
|
<a href="/home/profile/{tweet.name}" class="user">
|
||||||
|
{tweet.name}
|
||||||
|
</a>
|
||||||
|
<span class="handle">{tweet.handle}</span>
|
||||||
|
<span class="posted"> · {tweet.posted}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tweet">
|
||||||
|
<div class="content">
|
||||||
|
{tweet.content}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<form action="/home/like" method="post">
|
||||||
|
<input type="hidden" name="id" value={tweet.id} />
|
||||||
|
<button
|
||||||
|
class="btn like"
|
||||||
|
title="Like"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<div class="circle">
|
||||||
|
<Icon
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
name="like"
|
||||||
|
class={tweet.liked ? 'liked' : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="count">
|
||||||
|
{#key tweet.likes}
|
||||||
|
{#if tweet.likes}
|
||||||
|
<div
|
||||||
|
in:fly={{ y: 40 }}
|
||||||
|
out:fly={{ y: 40 }}
|
||||||
|
>
|
||||||
|
{tweet.likes}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/home/profile/{tweet.name}/status/{tweet.url}"
|
||||||
|
class="permalink"
|
||||||
|
title="Permalink"
|
||||||
|
>
|
||||||
|
<div class="circle">
|
||||||
|
<Icon width="24" height="24" name="permalink" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<form action="/home?_method=delete" method="post">
|
||||||
|
<input type="hidden" name="id" value={tweet.id} />
|
||||||
|
<button
|
||||||
|
aria-label="Remove tweet"
|
||||||
|
class="btn remove"
|
||||||
|
title="Remove"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<div class="circle">
|
||||||
|
<Icon width="24" height="24" name="remove" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.avatar {
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: var(--font-16);
|
||||||
|
padding: var(--spacing-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-container:hover {
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
gap: var(--spacing-16);
|
||||||
|
padding: var(--spacing-16) var(--spacing-24);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-container:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-details {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle,
|
||||||
|
.posted {
|
||||||
|
font-size: var(--font-16);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: var(--font-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-32);
|
||||||
|
margin-top: var(--spacing-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button,
|
||||||
|
.actions a {
|
||||||
|
padding: 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
background: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
padding: var(--spacing-16);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle > :global(.liked) {
|
||||||
|
color: hsl(9 100% 64%);
|
||||||
|
fill: hsl(9 100% 64%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.like {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.like:hover {
|
||||||
|
color: hsl(9 100% 64%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.like:hover .circle {
|
||||||
|
background: hsla(9 100% 64% / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.permalink:hover {
|
||||||
|
color: hsl(120 100% 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.permalink:hover .circle {
|
||||||
|
background-color: hsla(120 100% 50% / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove:hover {
|
||||||
|
color: hsl(0 100% 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove:hover .circle {
|
||||||
|
background-color: hsla(0 100% 50% / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.like,
|
||||||
|
.remove,
|
||||||
|
.permalink {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
margin-left: var(--spacing-16);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
src/lib/date.ts
Normal file
24
src/lib/date.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
export function timePosted(createdAt: Date): string {
|
||||||
|
try {
|
||||||
|
const posted = new Date(createdAt).getTime()
|
||||||
|
const currentTime = new Date().getTime()
|
||||||
|
const difference = currentTime - posted
|
||||||
|
const seconds = difference / 1000
|
||||||
|
const minutes = seconds / 60
|
||||||
|
const hours = minutes / 60
|
||||||
|
|
||||||
|
if (minutes <= 60) {
|
||||||
|
return `${minutes.toFixed()}m`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours <= 24) {
|
||||||
|
return `${hours.toFixed()}h`
|
||||||
|
}
|
||||||
|
|
||||||
|
return Intl.DateTimeFormat('en-US', {
|
||||||
|
dateStyle: 'medium'
|
||||||
|
}).format(posted)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`💩 Something went wrong: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/lib/prisma.ts
Normal file
5
src/lib/prisma.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
export default prisma
|
||||||
10
src/routes/api/index.svelte
Normal file
10
src/routes/api/index.svelte
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script>
|
||||||
|
export let item
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{item}</h1>
|
||||||
|
|
||||||
|
<form action="/api" method="post">
|
||||||
|
<input type="text" name="item" />
|
||||||
|
<button class="btn" type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
27
src/routes/api/index.ts
Normal file
27
src/routes/api/index.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
let item = 'banana'
|
||||||
|
|
||||||
|
export function get() {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function post({ request }) {
|
||||||
|
const form = await request.formData()
|
||||||
|
const newItem = form.get('item')
|
||||||
|
|
||||||
|
item = newItem;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 303,
|
||||||
|
headers: {
|
||||||
|
location: '/api'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Tweet from '$root/components/tweet.svelte'
|
||||||
|
import type { TweetType } from '$root/types'
|
||||||
|
|
||||||
|
export let tweets: TweetType[] = []
|
||||||
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Home</title>
|
<title>Home</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1>Feed</h1>
|
<h1>Feed</h1>
|
||||||
|
|
||||||
|
{#each tweets as tweet (tweet.id)}
|
||||||
|
<Tweet {tweet} />
|
||||||
|
{/each}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h1 {
|
h1 {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|
|
||||||
43
src/routes/home/index.ts
Normal file
43
src/routes/home/index.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import type { RequestHandler } from "@sveltejs/kit";
|
||||||
|
import prisma from "$root/lib/prisma";
|
||||||
|
import { timePosted } from "$root/lib/date";
|
||||||
|
|
||||||
|
export const get: RequestHandler = async () => {
|
||||||
|
const data = await prisma.tweet.findMany({
|
||||||
|
include: { user: true },
|
||||||
|
orderBy: { posted: 'desc' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const liked = await prisma.liked.findMany({
|
||||||
|
where: { userId: 1 },
|
||||||
|
select: { tweetId: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
const likedTweets = Object.keys(liked).map(
|
||||||
|
key => liked[key].tweetId
|
||||||
|
)
|
||||||
|
|
||||||
|
const tweets = data.map(tweet => {
|
||||||
|
return {
|
||||||
|
id: tweet.id,
|
||||||
|
content: tweet.content,
|
||||||
|
likes: tweet.likes,
|
||||||
|
posted: timePosted(tweet.posted),
|
||||||
|
url: tweet.url,
|
||||||
|
avatar: tweet.user.avatar,
|
||||||
|
handle: tweet.user.handle,
|
||||||
|
name: tweet.user.name,
|
||||||
|
liked: likedTweets.includes(tweet.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!tweets) {
|
||||||
|
return { status: 400 }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: { 'Content-Type': 'application/json ' },
|
||||||
|
status: 200,
|
||||||
|
body: { tweets }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/types/index.ts
Normal file
11
src/types/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export type TweetType = {
|
||||||
|
id: number
|
||||||
|
content: string
|
||||||
|
likes: number
|
||||||
|
posted: string
|
||||||
|
url: string
|
||||||
|
avatar: string
|
||||||
|
handle: string
|
||||||
|
name: string
|
||||||
|
liked: boolean
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue