Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions FrameRate/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Kopier til .env.local for å endre innstillinger lokalt. .env.local committes ikke.

# Adressen til GraphQL-backenden. Standard er backenden på egen maskin.
VITE_GRAPHQL_URL=http://localhost:3001/
150 changes: 149 additions & 1 deletion FrameRate/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion FrameRate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
"format:check": "prettier --check ."
},
"dependencies": {
"@apollo/client": "^4.3.5",
"graphql": "^16.14.2",
"react": "^19.2.8",
"react-dom": "^19.2.8"
"react-dom": "^19.2.8",
"rxjs": "^7.8.2"
},
"devDependencies": {
"@babel/core": "^7.29.7",
Expand Down
2 changes: 2 additions & 0 deletions FrameRate/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import './index.css';
import { Layout } from './components/layout/Layout';
import { FeaturedMovie } from './components/FeaturedMovie';
import { MovieSearch } from './components/search/MovieSearch';

function App() {
return (
<Layout>
<FeaturedMovie />
<MovieSearch />
</Layout>
);
}
Expand Down
14 changes: 14 additions & 0 deletions FrameRate/src/apollo/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';

// Adressen til GraphQL-backenden. Settes i .env.local (lokalt) eller ved bygging for VM-en,
// se .env.example. Uten innstilling brukes backenden på egen maskin.
const GRAPHQL_URL = import.meta.env.VITE_GRAPHQL_URL ?? 'http://localhost:3001/';

/**
* Apollo Client henter data fra backenden og fungerer også som appens state management:
* svarene lagres i en cache, så samme spørring ikke sendes på nytt når brukeren går frem og tilbake.
*/
export const client = new ApolloClient({
link: new HttpLink({ uri: GRAPHQL_URL }),
cache: new InMemoryCache(),
});
83 changes: 56 additions & 27 deletions FrameRate/src/components/FeaturedMovie.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,69 @@
import { featuredMovie as movie, featuredReview as review, genres } from '../data/mockData';
import { useQuery } from '@apollo/client/react';
import { SEARCH_MOVIES, posterUrl } from '../graphql/movies';

const IMAGE_BASE = 'https://image.tmdb.org/t/p';
const DICE = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];

/**
* Filmen med høyest terningkast fra våre egne anmeldelser.
* Før noen har anmeldt noe, velger backenden den mest populære filmen i stedet.
*/
export function FeaturedMovie() {
const year = movie.release_date.slice(0, 4);
const genreNames = movie.genre_ids.map(id => genres[id]);
const rounded = Math.round(movie.average_rating);
const { data, loading, error } = useQuery(SEARCH_MOVIES, {
variables: { sort: 'RATING_DESC', limit: 1 },
});

const movie = data?.searchMovies.movies[0];

if (loading) {
return (
<section className="featured" aria-busy="true">
<p className="featured-label">Henter utvalgt film …</p>
</section>
);
}

if (error || !movie) {
return (
<section className="featured">
<p className="featured-label">Kunne ikke hente utvalgt film akkurat nå.</p>
</section>
);
}

const rating = movie.averageRating;

return (
<section className="featured">
{/* Venstre: tekst */}
<div className="featured-content">
<p className="featured-label">Utvalgt fra topplisten</p>
<p className="featured-label">
{rating != null ? 'Utvalgt fra topplisten' : 'Utvalgt film'}
</p>
<h1 className="featured-title">{movie.title}</h1>

<p className="featured-meta">
<span>{year}</span>
<span>{genreNames.join(' / ')}</span>
{movie.releaseYear && <span>{movie.releaseYear}</span>}
<span>{movie.genres.map(g => g.name).join(' / ')}</span>
</p>

<div className="featured-rating">
<span className="featured-dice" aria-hidden="true">
{DICE[rounded - 1]}
</span>
{rating != null && (
<span className="featured-dice" aria-hidden="true">
{DICE[Math.round(rating) - 1]}
</span>
)}
<div>
<p className="featured-rating-title">Terningkast {movie.average_rating.toFixed(1)}</p>
<p className="featured-rating-count">Basert på {movie.review_count} anmeldelser</p>
<p className="featured-rating-title">
{rating != null ? `Terningkast ${rating.toFixed(1)}` : 'Ingen anmeldelser ennå'}
</p>
<p className="featured-rating-count">
{rating != null
? `Basert på ${movie.reviewCount} anmeldelser`
: 'Bli den første som gir terningkast!'}
</p>
</div>
</div>

<blockquote className="featured-quote">
<p>«{review.text}»</p>
<footer>
— {review.author}, {review.date}
</footer>
</blockquote>

<div className="featured-buttons">
<a href={`/film/${movie.id}`} className="button-white">
Les mer om filmen
Expand All @@ -48,13 +75,15 @@ export function FeaturedMovie() {
</div>

{/* Høyre: plakat */}
<img
className="featured-poster"
src={`${IMAGE_BASE}/w342${movie.poster_path}`}
alt={`Plakat for ${movie.title}`}
width="342"
height="513"
/>
{movie.posterPath && (
<img
className="featured-poster"
src={posterUrl(movie.posterPath, 'w342')}
alt={`Plakat for ${movie.title}`}
width="342"
height="513"
/>
)}
</section>
);
}
Loading